Skip to content

Commit

Permalink
feat(GeoFirestore): add ability to update documents
Browse files Browse the repository at this point in the history
refactor(GeoFirestore): remove merge from batch.set

docs(GeoFirestore): add docs for `update` function of `GeoFirestore`

test(GeoFirestore): add tests for `update`,validateDoc..HasCoordinates`
  • Loading branch information
Daan committed Dec 6, 2018
1 parent d71e1bc commit 2f0b127
Show file tree
Hide file tree
Showing 6 changed files with 1,266 additions and 12 deletions.
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ You can find a full list of our demos and view the code for each of them in the
* [`get(key)`](#geofirestoregetkey)
* [`add(document[, customKey])`](#geofirestoreadddocument-customkey)
* [`set(keyOrDocuments[, document, customKey])`](#geofirestoresetkeyordocuments-document-customkey)
* [`update(keyOrDocuments[, document, customKey])`](#geofirestoreupdateexistingkeyordocuments-document-customkey)
* [`remove(key)`](#geofirestoreremovekey)
* [`query(queryCriteria)`](#geofirestorequeryquerycriteria)
* [`GeoFirestoreQuery`](#geofirestorequery)
Expand Down Expand Up @@ -157,6 +158,53 @@ geoFirestore.set({
});
```

#### GeoFirestore.update(existingKeyOrDocuments[, document, customKey])

Updates the specified key - document pair(s) in this `GeoFirestore`. If the provided `keyOrDocuments` argument is a string than a single `document` will be updated. The `keyOrDocuments` argument can also an object containing a mapping between keys and documents. Thus allowing you to add several documents to GeoFirestore in one write.

Additional attributes can be updated or added to the `keyOrDocuments`. If any of the attributes pre-exist in the `document` -and are not (re)defined within the `keyOrDocuments` argument-, then those attributes will remain (unchanged). If merging of existing `document` data is not required, then [`GeoFirestore.set([..])`](#geofirestoresetkeyordocuments-document-customkey) is the more efficient choice.

This method returns a promise which is fulfilled when the new document has been synchronized with the Firebase servers. If the provided key(s) do not exist in `GeoFirestore`, the update will fail and the returned Promise will be rejected. If the document has a `coordinates` field to update, then it must be a valid Firestore GeoPoint.

```JavaScript
geoFirestore.update('existing_key', {
coordinates: new firebase.firestore.GeoPoint(37.79, -122.41),
some_custom_key: 'text' // Add|update a custom key
}).then(() => {
console.log('Document has been updated in GeoFirestore');
}, (error) => {
console.log('Error: ' + error);
});
```

```JavaScript
geoFirestore.update('existing_key', {
// Update coordinates only
coordinates: new firebase.firestore.GeoPoint(52.09, -5.12)
}).then(() => {
console.log('Document has been updated in GeoFirestore');
}, (error) => {
console.log('Error: ' + error);
});
```

```JavaScript
geoFirestore.update({
// Update multiple documents (with && witout coordinates)
'existing_key': {
custom_key: 'new text'
},
'another_existing_key': {
coordinates: new firebase.firestore.GeoPoint(36.98, -122.56),
other_key: 'text'
}
}).then(() => {
console.log('Provided documents have been updated in GeoFirestore');
}, (error) => {
console.log('Error: ' + error);
});
```

#### GeoFirestore.remove(key)

Removes the provided `key` from this `GeoFirestore`. Returns a promise fulfilled when the removal of `key` has been synchronized with the Firebase servers. If the provided `key` is not present in this `GeoFirestore`, the promise will still successfully resolve.
Expand Down
62 changes: 55 additions & 7 deletions src/geofirestore.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { GeoFirestoreQuery } from './query';
import { decodeGeoFirestoreObject, degreesToRadians, encodeGeoFireObject, encodeGeohash, validateLocation, validateKey, findCoordinatesKey } from './utils';
import { decodeGeoFirestoreObject, degreesToRadians, encodeGeoFireObject, geoFireDocToFieldPath, encodeGeohash, validateLocation, validateKey, validateDocumentHasCoordinates, findCoordinatesKey } from './utils';

import { firestore, GeoFirestoreObj, QueryCriteria } from './interfaces';

Expand Down Expand Up @@ -96,21 +96,30 @@ export class GeoFirestore {
*
* If any provided key already exists in this GeoFirestore, it will be overwritten with the new location value.
*
* @param writeType Specifies the type of write (set || update).
* @param keyOrDocuments The key representing the document to add or an object of $key - document pairs.
* @param document The document to be added to the GeoFirestore.
* @param customKey The key of the document to use as the location. Otherwise we default to `coordinates`.
* @returns A promise that is fulfilled when the write is complete.
*/
public set(keyOrDocuments: string | any, document?: any, customKey?: string): Promise<any> {
private write(writeType: string, keyOrDocuments: string | any, document?: any, customKey?: string): Promise<any> {
if (typeof keyOrDocuments === 'string' && keyOrDocuments.length !== 0) {
validateKey(keyOrDocuments);
if (!document) {
// Setting location to null is valid since it will remove the key
return this._collectionRef.doc(keyOrDocuments).delete();
} else {
if(writeType === 'update' && !validateDocumentHasCoordinates(document, customKey)){
return this._collectionRef.doc(keyOrDocuments).update(geoFireDocToFieldPath({d: document}));
}
const locationKey: string = findCoordinatesKey(document, customKey);
const location: firestore.web.GeoPoint | firestore.cloud.GeoPoint = document[locationKey];
const geohash: string = encodeGeohash(location);
const geohash: string = encodeGeohash(location);

if(writeType === 'update') {
const geoFireDocument = encodeGeoFireObject(location, geohash, document);
return this._collectionRef.doc(keyOrDocuments).update(geoFireDocToFieldPath(geoFireDocument));
}
return this._collectionRef.doc(keyOrDocuments).set(encodeGeoFireObject(location, geohash, document));
}
} else if (typeof keyOrDocuments === 'object') {
Expand All @@ -129,10 +138,19 @@ export class GeoFirestore {
if (!documentToUpdate) {
batch.delete(ref);
} else {
const locationKey = findCoordinatesKey(documentToUpdate, customKey);
const location: firestore.web.GeoPoint | firestore.cloud.GeoPoint = documentToUpdate[locationKey];
const geohash: string = encodeGeohash(location);
batch.set(ref, encodeGeoFireObject(location, geohash, documentToUpdate), { merge: true });
if(writeType === 'update' && !validateDocumentHasCoordinates(documentToUpdate, customKey)){
batch.update(ref, geoFireDocToFieldPath({d: documentToUpdate}));
} else {
const locationKey = findCoordinatesKey(documentToUpdate, customKey);
const location: firestore.web.GeoPoint | firestore.cloud.GeoPoint = documentToUpdate[locationKey];
const geohash: string = encodeGeohash(location);
if(writeType === 'update') {
const geoFireDocument = encodeGeoFireObject(location, geohash, documentToUpdate);
batch.update(ref, geoFireDocToFieldPath(geoFireDocument));
}else {
batch.set(ref, encodeGeoFireObject(location, geohash, documentToUpdate));
}
}
}
});
return batch.commit();
Expand All @@ -148,6 +166,36 @@ export class GeoFirestore {
return new GeoFirestoreQuery(this._collectionRef, queryCriteria);
}

/**
* Adds the provided key - location pair(s) to Firestore. Returns an empty promise which is fulfilled when the write is complete.
*
* If any provided key already exists in this GeoFirestore, it will be overwritten with the new location value.
*
* @param keyOrDocuments The key representing the document to add or an object of $key - document pairs.
* @param document The document to be added to the GeoFirestore.
* @param customKey The key of the document to use as the location. Otherwise we default to `coordinates`.
* @returns A promise that is fulfilled when the write is complete.
*/

public set (keyOrDocuments: string | any, document?: any, customKey?: string): Promise<any> {
return this.write('set', keyOrDocuments, document, customKey);
}
/**
* Updates the provided key - location pair(s) to Firestore. Returns an empty promise which is fulfilled when the write is complete.
*
* If any provided key already exists in this GeoFirestore, it will be overwritten with the new location value.
*
* @param keyOrDocuments The key representing the document to add or an object of $key - document pairs.
* @param document The document to be added to the GeoFirestore.
* @param customKey The key of the document to use as the location. Otherwise we default to `coordinates`.
* @returns A promise that is fulfilled when the write is complete.
*/

public update (keyOrDocuments: string | any, document?: any, customKey?: string): Promise<any> {
return this.write('update', keyOrDocuments, document, customKey);
}


/********************/
/* STATIC METHODS */
/********************/
Expand Down
55 changes: 54 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { firestore, GeoFirestoreObj, QueryCriteria } from './interfaces';
import { triggerAsyncId } from 'async_hooks';

// Default geohash length
export const GEOHASH_PRECISION = 10;
Expand Down Expand Up @@ -199,6 +200,28 @@ export function validateCriteria(newQueryCriteria: QueryCriteria, requireCenterA
}
}

/**
* Validates if coordinates are defined in document.
* @param document A GeoFirestore document.
* @param customKey A custom keyname that might contain coordinates.
* @returns True if a location field can be found in the document.
*/
export function validateDocumentHasCoordinates(document: any, customKey?: string): boolean {

if (
(document && typeof document === 'object') // ${document} has to be defined (..in 'object')
&& // (AND)
(
(customKey && customKey in document) // ${customKey} has to be defined (..in ${document})
|| // (OR)
('coordinates' in document) // 'coordinates' has to be defined in ${document}
)
) {
return true;
}
return false;
}

/**
* Converts degrees to radians.
*
Expand Down Expand Up @@ -462,6 +485,35 @@ export function decodeGeoFirestoreObject(geoFirestoreObj: GeoFirestoreObj): any
}
}

/**
* This function exists because Firestore can update fieldpaths but not nested documents
*
* @param geoFireObject Encoded and GeoFire object
* @returns GeoFire object with obj[d.field] instead of obj.d.field
*
* @summary
* "With update() you can also use field paths for updating nested values" --Scarygami
*
* We need this because doc['d.fieldname'] = 1; will add/update d.fieldname,
* doc.d.fieldnname = 1; will empty doc.d so only doc.d.fieldnname remains
*
* "For set() you always have to provide document-shaped data" --Scarygami
*
* If we do this than d. is not updated but replaced even if set([..], {merge: true})
*/
export function geoFireDocToFieldPath(geoFireObject: any): GeoFirestoreObj {
if((typeof (geoFireObject.d !== 'undefined')) &&
(geoFireObject.d.length !== 0)
){
const document = geoFireObject.d;
delete geoFireObject.d;
Object.keys(document).forEach((key) =>{
geoFireObject[`d.${key}`] = document[key];
});
}
return geoFireObject;
}

/**
* Returns the id of a Firestore snapshot across SDK versions.
*
Expand All @@ -480,9 +532,10 @@ export function geoFirestoreGetKey(snapshot: firestore.web.DocumentSnapshot | fi
* Returns the key of a document that is a GeoPoint.
*
* @param document A GeoFirestore document.
* @param customKey A custom keyname that might contain coordinates.
* @returns The key for the location field of a document.
*/
export function findCoordinatesKey(document: any, customKey?: string): string {
export function findCoordinatesKey(document: any, customKey?: string) {
let error: string;
let key: string;

Expand Down
3 changes: 2 additions & 1 deletion test/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const expect = chai.expect;
// Define examples of valid and invalid parameters
export const invalidFirebaseRefs = [null, undefined, NaN, true, false, [], 0, 5, '', 'a', ['hi', 1]];
export const validKeys = ['a', 'loc1', '(e@Xi:4t>*E2)hc<5oa:1s6{B0d?u', new Array(700).join('a')];
export const invalidKeys = ['', true, false, null, undefined, { a: 1 }, 'loc.1', 'loc$1', '[loc1', 'loc1]', 'loc#1', 'loc/1', 'a#i]$da[s', 'te/nst', 'te/rst', 'te/u0000st', 'te/u0015st', 'te/007Fst', new Array(800).join('a')];
export const invalidKeys = ['', true, false, null, undefined, { a: 1 }, 'loc.1', 'loc$1', '[loc1', 'loc1]', 'loc#1', 'loc/1', 'a#i]$da[s', 'te/nst', 'te/rst', 'te/u0000st', 'te/u0015st', 'te/007Fst','__.*__', new Array(800).join('a')];
export const validLocations = [new firebase.firestore.GeoPoint(0, 0), new firebase.firestore.GeoPoint(-90, 180), new firebase.firestore.GeoPoint(90, -180), new firebase.firestore.GeoPoint(23, 74), new firebase.firestore.GeoPoint(47.235124363, 127.2379654226)];
export const invalidLocations = [{ latitude: -91, longitude: 0 }, { latitude: 91, longitude: 0 }, { latitude: 0, longitude: 181 }, { latitude: 0, longitude: -181 }, { latitude: [0, 0], longitude: 0 }, { latitude: 'a', longitude: 0 }, { latitude: 0, longitude: 'a' }, { latitude: 'a', longitude: 'a' }, { latitude: NaN, longitude: 0 }, { latitude: 0, longitude: NaN }, { latitude: undefined, longitude: NaN }, { latitude: null, longitude: 0 }, { latitude: null, longitude: null }, { latitude: 0, longitude: undefined }, { latitude: undefined, longitude: undefined }, '', 'a', true, false, [], [1], {}, { a: 1 }, null, undefined, NaN];
export const invalidObjects = [false, true, 'pie', 3, null, undefined, NaN];
Expand Down Expand Up @@ -137,6 +137,7 @@ export class Checklist {
* went wrong.
**/
export function failTestOnCaughtError(error) {
console.log(error);
expect(error).to.throw();
}

Expand Down
Loading

0 comments on commit 2f0b127

Please sign in to comment.