Skip to content

Commit

Permalink
feat(GeoDocumentReference): add GeoDocumentReference for returns of d…
Browse files Browse the repository at this point in the history
…oc and add
  • Loading branch information
MichaelSolati committed Oct 4, 2018
1 parent 3cd3e0e commit 9a5af9a
Show file tree
Hide file tree
Showing 6 changed files with 254 additions and 73 deletions.
44 changes: 35 additions & 9 deletions src/collection.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { GeoFirestoreTypes, DocumentData } from './interfaces';
import { GeoFirestoreTypes } from './interfaces';
import { GeoDocumentReference } from './documentReference';
import { GeoQuery } from './query';
import { findCoordinatesKey, encodeGeohash, encodeGeoDocument } from './utils';

Expand All @@ -17,32 +18,57 @@ export class GeoCollectionReference extends GeoQuery {
}
}

/** The identifier of the collection. */
get id(): string {
return this._collection.id;
}

/**
* A reference to the containing Document if this is a subcollection, else null.
*/
get parent(): GeoDocumentReference | null {
return this._collection.parent ? new GeoDocumentReference(this._collection.parent) : null;
}

/**
* Gets a `CollectionReference` instance of the collection used by the GeoCollectionReference. Using this object for queries and other
* commands WILL NOT take advantage of GeoFirestore's geo based logic.
* A string representing the path of the referenced collection (relative
* to the root of the database).
*/
get collection(): GeoFirestoreTypes.cloud.CollectionReference | GeoFirestoreTypes.web.CollectionReference {
return this._collection;
get path(): string {
return this._collection.path;
}

/**
* Add a new document to this collection with the specified data, assigning it a document ID automatically.
*
* @param data An Object containing the data for the new document.
* @param customKey The key of the document to use as the location. Otherwise we default to `coordinates`.
* @return A Promise resolved with a `DocumentReference` pointing to the newly created document after it has been written to the backend.
* @return A Promise resolved with a `GeoDocumentReference` pointing to the newly created document after it has been written to the
* backend.
*/
public add(
data: DocumentData,
data: GeoFirestoreTypes.DocumentData,
customKey?: string
): Promise<GeoFirestoreTypes.cloud.DocumentReference> | Promise<GeoFirestoreTypes.web.DocumentReference> {
): Promise<GeoDocumentReference> {
if (Object.prototype.toString.call(data) === '[object Object]') {
const locationKey: string = findCoordinatesKey(data, customKey);
const location: GeoFirestoreTypes.cloud.GeoPoint | GeoFirestoreTypes.web.GeoPoint = data[locationKey];
const geohash: string = encodeGeohash(location);
return this._collection.add(encodeGeoDocument(location, geohash, data));
return (this._collection as GeoFirestoreTypes.cloud.CollectionReference)
.add(encodeGeoDocument(location, geohash, data)).then(doc => new GeoDocumentReference(doc));
} else {
throw new Error('document must be an object');
}
}

/**
* Get a `GeoDocumentReference` for the document within the collection at the specified path. If no path is specified, an
* automatically-generated unique ID will be used for the returned GeoDocumentReference.
*
* @param documentPath A slash-separated path to a document.
* @return The `GeoDocumentReference` instance.
*/
public doc(documentPath?: string): GeoDocumentReference {
return new GeoDocumentReference(this._collection.doc(documentPath));
}
}
157 changes: 157 additions & 0 deletions src/documentReference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { GeoFirestoreTypes } from './interfaces';
import { encodeSetUpdateDocument } from './utils';
import { GeoCollectionReference } from './collection';
import { GeoFirestore } from './firestore';

/**
* A `GeoDocumentReference` refers to a document location in a Firestore database and can be used to write, read, or listen to the
* location. The document at the referenced location may or may not exist. A `GeoDocumentReference` can also be used to create a
* `CollectionReference` to a subcollection.
*/
export class GeoDocumentReference {
private _isWeb: boolean;

/**
* @param _document The `DocumentReference` instance.
*/
constructor(private _document: GeoFirestoreTypes.cloud.DocumentReference | GeoFirestoreTypes.web.DocumentReference) {
if (Object.prototype.toString.call(_document) !== '[object Object]') {
throw new Error('DocumentReference must be an instance of a Firestore DocumentReference');
}
this._isWeb = Object.prototype.toString
.call((_document as GeoFirestoreTypes.web.DocumentReference).firestore.enablePersistence) === '[object Function]';
}

/** The identifier of the document within its collection. */
get id(): string {
return this._document.id;
}

/**
* The `GeoFirestore` for the Firestore database (useful for performing transactions, etc.).
*/
get firestore(): GeoFirestore {
return new GeoFirestore(this._document.firestore);
}

/**
* Attaches a listener for DocumentSnapshot events. You may either pass individual `onNext` and `onError` callbacks.
*
* @param onNext A callback to be called every time a new `DocumentSnapshot` is available.
* @param onError A callback to be called if the listen fails or is cancelled. No further callbacks will occur.
* @return An unsubscribe function that can be called to cancel the snapshot listener.
*/
get onSnapshot(): (
onNext?: (snapshot: GeoFirestoreTypes.cloud.DocumentSnapshot | GeoFirestoreTypes.web.DocumentSnapshot) => void,
onError?: (error: Error) => void,
) => void {
return (
onNext?: (snapshot: GeoFirestoreTypes.cloud.DocumentSnapshot | GeoFirestoreTypes.web.DocumentSnapshot) => void,
onError?: (error: Error) => void,
) => {
return (this._document as GeoFirestoreTypes.web.DocumentReference).onSnapshot((snapshot) => {
if (onNext) { onNext(snapshot); }
}, (error) => {
if (onError) { onError(error); }
});
};
}

/**
* A reference to the GeoCollection to which this GeoDocumentReference belongs.
*/
get parent(): GeoCollectionReference {
return new GeoCollectionReference(this._document.parent);
}

/**
* A string representing the path of the referenced document (relative to the root of the database).
*/
get path(): string {
return this._document.path;
}

/**
* Gets a `GeoCollectionReference` instance that refers to the collection at the specified path.
*
* @param collectionPath A slash-separated path to a collection.
* @return The `GeoCollectionReference` instance.
*/
public collection(collectionPath: string): GeoCollectionReference {
return new GeoCollectionReference(this._document.collection(collectionPath));
}

/**
* Returns true if this `GeoDocumentReference` is equal to the provided one.
*
* @param other The `DocumentReference` or `GeoDocumentReference` to compare against.
* @return true if this `DocumentReference` or `GeoDocumentReference` is equal to the provided one.
*/
public isEqual(
other: GeoDocumentReference | GeoFirestoreTypes.cloud.DocumentReference | GeoFirestoreTypes.web.DocumentReference
): boolean {
if (other instanceof GeoDocumentReference) {
return (this._document as GeoFirestoreTypes.web.DocumentReference)
.isEqual(other['_document'] as GeoFirestoreTypes.web.DocumentReference);
}
return (this._document as GeoFirestoreTypes.web.DocumentReference).isEqual(other as GeoFirestoreTypes.web.DocumentReference);
}

/**
* Writes to the document referred to by this `GeoDocumentReference`. If the document does not yet exist, it will be created. If you pass
* `SetOptions`, the provided data can be merged into an existing document.
*
* @param data A map of the fields and values for the document.
* @param options An object to configure the set behavior. Includes custom key for location in document.
* @return A Promise resolved once the data has been successfully written to the backend (Note it won't resolve while you're offline).
*/
public set(
data: GeoFirestoreTypes.DocumentData,
options?: GeoFirestoreTypes.cloud.SetOptions | GeoFirestoreTypes.web.SetOptions
): Promise<void> {
return (this._document as GeoFirestoreTypes.web.DocumentReference).set(
encodeSetUpdateDocument(data, (options) ? options.customKey : null),
options
).then(() => null);
}

/**
* Updates fields in the document referred to by this `GeoDocumentReference`. The update will fail if applied to a document that does not
* exist.
*
* @param data An object containing the fields and values with which to update the document. Fields can contain dots to reference nested
* fields within the document.
* @param customKey The key of the document to use as the location. Otherwise we default to `coordinates`.
* @return A Promise resolved once the data has been successfully written to the backend (Note it won't resolve while you're offline).
*/
public update(data: GeoFirestoreTypes.UpdateData, customKey?: string): Promise<void> {
return (this._document as GeoFirestoreTypes.web.DocumentReference).update(encodeSetUpdateDocument(data, customKey)).then(() => null);
}

/**
* Deletes the document referred to by this `GeoDocumentReference`.
*
* @return A Promise resolved once the document has been successfully deleted from the backend (Note that it won't resolve while you're
* offline).
*/
public delete(): Promise<void> {
return (this._document as GeoFirestoreTypes.web.DocumentReference).delete().then(() => null);
}

/**
* Reads the document referred to by this `GeoDocumentReference`.
*
* Note: By default, get() attempts to provide up-to-date data when possible by waiting for data from the server, but it may return
* cached data or fail if you are offline and the server cannot be reached. This behavior can be altered via the `GetOptions` parameter.
*
* @param options An object to configure the get behavior.
* @return A Promise resolved with a DocumentSnapshot containing the current document contents.
*/
public get(
options: GeoFirestoreTypes.web.GetOptions = { source: 'default' }
): Promise<GeoFirestoreTypes.cloud.DocumentSnapshot | GeoFirestoreTypes.web.DocumentSnapshot> {
return this._isWeb ?
(this._document as GeoFirestoreTypes.web.DocumentReference).get(options) :
this._document.get();
}
}
21 changes: 11 additions & 10 deletions src/interfaces/firestore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,22 @@ import * as FirestoreTypes from '@firebase/firestore-types';
import '@google-cloud/firestore/types/firestore';
import '@types/node';

import { GeoDocument } from './document';
import { GeoDocumentChange } from './documentChange';
import { GeoQueryCriteria } from './queryCriteria';
import { GeoQueryDocumentSnapshot } from './queryDocumentSnapshot';
import { GeoDocument as IGeoDocument } from './document';
import { GeoDocumentChange as IGeoDocumentChange } from './documentChange';
import { DocumentData as IDocumentData } from './documentData';
import { GeoQueryCriteria as IGeoQueryCriteria } from './queryCriteria';
import { GeoQueryDocumentSnapshot as IGeoQueryDocumentSnapshot } from './queryDocumentSnapshot';

export namespace GeoFirestoreTypes {
export interface Document extends GeoDocument { }
export interface DocumentChange extends GeoDocumentChange { }
export interface QueryCriteria extends GeoQueryCriteria { }
export interface QueryDocumentSnapshot extends GeoQueryDocumentSnapshot { }
export interface Document extends IGeoDocument { }
export interface DocumentData extends IDocumentData { }
export interface DocumentChange extends IGeoDocumentChange { }
export interface QueryCriteria extends IGeoQueryCriteria { }
export interface QueryDocumentSnapshot extends IGeoQueryDocumentSnapshot { }
export interface UpdateData extends IDocumentData { }
export namespace web {
export interface CollectionReference extends FirestoreTypes.CollectionReference { }
export interface DocumentChange extends FirestoreTypes.DocumentChange { }
export interface DocumentData extends FirestoreTypes.DocumentData { }
export interface DocumentReference extends FirestoreTypes.DocumentReference { }
export interface DocumentSnapshot extends FirestoreTypes.DocumentSnapshot { }
export interface Firestore extends FirestoreTypes.FirebaseFirestore { }
Expand All @@ -35,7 +37,6 @@ export namespace GeoFirestoreTypes {
export namespace cloud {
export interface CollectionReference extends FirebaseFirestore.CollectionReference { }
export interface DocumentChange extends FirebaseFirestore.DocumentChange { }
export interface DocumentData extends FirebaseFirestore.DocumentData { }
export interface DocumentReference extends FirebaseFirestore.DocumentReference { }
export interface DocumentSnapshot extends FirebaseFirestore.DocumentSnapshot { }
export interface Firestore extends FirebaseFirestore.Firestore { }
Expand Down
19 changes: 5 additions & 14 deletions src/query.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { GeoFirestoreTypes } from './interfaces';
import { GeoFirestore } from './firestore';
import { GeoQuerySnapshot } from './querySnapshot';
import { validateQueryCriteria, geohashQueries } from './utils';

Expand Down Expand Up @@ -42,8 +43,8 @@ export class GeoQuery {
/**
* The `Firestore` for the Firestore database (useful for performing transactions, etc.).
*/
get firestore(): GeoFirestoreTypes.cloud.Firestore | GeoFirestoreTypes.web.Firestore {
return this._query.firestore;
get firestore(): GeoFirestore {
return new GeoFirestore(this._query.firestore);
}

/**
Expand All @@ -70,11 +71,9 @@ export class GeoQuery {
const subscriptions: Array<() => void> = [];
this._generateQuery().forEach((value: GeoFirestoreTypes.web.Query) => {
const subscription = value.onSnapshot((snapshot) => {
onNext(new GeoQuerySnapshot(snapshot, this.geoQueryCriteria));
if (onNext) { onNext(new GeoQuerySnapshot(snapshot, this.geoQueryCriteria)); }
}, (error) => {
if (onError) {
onError(error);
}
if (onError) { onError(error); }
});
subscriptions.push(subscription);
});
Expand All @@ -85,14 +84,6 @@ export class GeoQuery {
};
}

/**
* Gets a `Query`, which you can read or listen to, used by the GeoQuery. Using this object for queries and other commands WILL NOT take
* advantage of GeoFirestore's geo based logic.
*/
get query(): GeoFirestoreTypes.cloud.Query | GeoFirestoreTypes.web.Query {
return this._query;
}

/**
* Returns the radius of this query, in kilometers.
*/
Expand Down
44 changes: 22 additions & 22 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,28 +142,6 @@ export function degreesToRadians(degrees: number): number {
return (degrees * Math.PI / 180);
}

/**
* Encodes a Document used by GeoWriteBatch as a GeoDocument.
*
* @param data The document being set or updated.
* @param customKey The key of the document to use as the location. Otherwise we default to `coordinates`.
* @return The document encoded as GeoDocument object.
*/
export function encodeBatchDocument(data: DocumentData, customKey?: string): GeoDocument {
if (Object.prototype.toString.call(data) === '[object Object]') {
const unparsed: DocumentData = ('d' in data) ? data.d : data;
const locationKey: string = findCoordinatesKey(unparsed, customKey, true);
if (locationKey) {
const location: GeoFirestoreTypes.cloud.GeoPoint | GeoFirestoreTypes.web.GeoPoint = unparsed[locationKey];
const geohash: string = encodeGeohash(location);
return encodeGeoDocument(location, geohash, unparsed);
}
return { d: unparsed } as GeoDocument;
} else {
throw new Error('document must be an object');
}
}

/**
* Generates a geohash of the specified precision/string length from the inputted GeoPoint.
*
Expand Down Expand Up @@ -244,6 +222,28 @@ export function encodeGeoDocument(
return { g: geohash, l: location, d: document };
}

/**
* Encodes a Document used by GeoWriteBatch as a GeoDocument.
*
* @param data The document being set or updated.
* @param customKey The key of the document to use as the location. Otherwise we default to `coordinates`.
* @return The document encoded as GeoDocument object.
*/
export function encodeSetUpdateDocument(data: DocumentData, customKey?: string): GeoDocument {
if (Object.prototype.toString.call(data) === '[object Object]') {
const unparsed: DocumentData = ('d' in data) ? data.d : data;
const locationKey: string = findCoordinatesKey(unparsed, customKey, true);
if (locationKey) {
const location: GeoFirestoreTypes.cloud.GeoPoint | GeoFirestoreTypes.web.GeoPoint = unparsed[locationKey];
const geohash: string = encodeGeohash(location);
return encodeGeoDocument(location, geohash, unparsed);
}
return { d: unparsed } as GeoDocument;
} else {
throw new Error('document must be an object');
}
}

/**
* Returns the key of a document that is a GeoPoint.
*
Expand Down
Loading

0 comments on commit 9a5af9a

Please sign in to comment.