From 246cc64223965dc48c06a77275130ff1cc1080e1 Mon Sep 17 00:00:00 2001 From: Michael Solati Date: Sat, 27 Jun 2020 22:31:37 -0700 Subject: [PATCH] refactor: move all exported functions to `/api` --- src/api/encode.ts | 94 +++++++++++++ src/api/query-get.ts | 7 +- src/api/query-on-snapshot.ts | 10 +- src/api/snapshot.ts | 3 +- src/api/validate.ts | 156 ++++++++++++++++++++++ src/index.ts | 15 ++- src/utils.ts | 246 +---------------------------------- 7 files changed, 269 insertions(+), 262 deletions(-) create mode 100644 src/api/encode.ts create mode 100644 src/api/validate.ts diff --git a/src/api/encode.ts b/src/api/encode.ts new file mode 100644 index 0000000..991b93e --- /dev/null +++ b/src/api/encode.ts @@ -0,0 +1,94 @@ +import {hash} from 'geokit'; +import {validateLocation} from './validate'; +import {GeoFirestoreTypes} from '../definitions'; +import {findGeoPoint} from '../utils'; + +/** + * Encodes a Firestore Document to be added as a GeoDocument. + * + * @param documentData The document being set. + * @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 encodeDocumentAdd( + documentData: GeoFirestoreTypes.DocumentData, + customKey?: string +): GeoFirestoreTypes.GeoDocumentData { + if (Object.prototype.toString.call(documentData) !== '[object Object]') { + throw new Error('document must be an object'); + } + const geopoint = findGeoPoint(documentData, customKey); + return encodeGeoDocument(geopoint, documentData); +} + +/** + * Encodes a Firestore Document to be set as a GeoDocument. + * + * @param documentData 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 The document encoded as GeoDocument object. + */ +export function encodeDocumentSet( + documentData: GeoFirestoreTypes.DocumentData, + options?: GeoFirestoreTypes.SetOptions +): GeoFirestoreTypes.GeoDocumentData | GeoFirestoreTypes.DocumentData { + if (Object.prototype.toString.call(documentData) !== '[object Object]') { + throw new Error('document must be an object'); + } + const customKey = options && options.customKey; + const geopoint = findGeoPoint( + documentData, + customKey, + options && (options.merge || !!options.mergeFields) + ); + if (geopoint) { + return encodeGeoDocument(geopoint, documentData); + } + return documentData; +} + +/** + * Encodes a Firestore Document to be updated as a GeoDocument. + * + * @param documentData The document being 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 encodeDocumentUpdate( + documentData: GeoFirestoreTypes.UpdateData, + customKey?: string +): GeoFirestoreTypes.UpdateData { + if (Object.prototype.toString.call(documentData) !== '[object Object]') { + throw new Error('document must be an object'); + } + const geopoint = findGeoPoint(documentData, customKey, true); + if (geopoint) { + documentData = encodeGeoDocument(geopoint, documentData); + } + return documentData; +} + +/** + * Encodes a document with a GeoPoint as a GeoDocument. + * + * @param geopoint The location as a Firestore GeoPoint. + * @param documentData Document to encode. + * @return The document encoded as GeoDocument object. + */ +export function encodeGeoDocument( + geopoint: GeoFirestoreTypes.cloud.GeoPoint | GeoFirestoreTypes.web.GeoPoint, + documentData: GeoFirestoreTypes.DocumentData +): GeoFirestoreTypes.GeoDocumentData { + validateLocation(geopoint); + const geohash = hash({ + lat: geopoint.latitude, + lng: geopoint.longitude, + }); + return { + ...documentData, + g: { + geopoint, + geohash, + }, + }; +} diff --git a/src/api/query-get.ts b/src/api/query-get.ts index 96ef636..3ea4019 100644 --- a/src/api/query-get.ts +++ b/src/api/query-get.ts @@ -1,10 +1,7 @@ import {GeoQuerySnapshot} from './snapshot'; +import {validateQueryCriteria} from './validate'; import {GeoFirestoreTypes} from '../definitions'; -import { - calculateDistance, - generateQuery, - validateQueryCriteria, -} from '../utils'; +import {calculateDistance, generateQuery} from '../utils'; /** * Executes a query and returns the result(s) as a GeoQuerySnapshot. diff --git a/src/api/query-on-snapshot.ts b/src/api/query-on-snapshot.ts index 8217388..d3ff311 100644 --- a/src/api/query-on-snapshot.ts +++ b/src/api/query-on-snapshot.ts @@ -1,11 +1,7 @@ -import {GeoFirestoreTypes} from '../definitions'; import {GeoQuerySnapshot} from './snapshot'; -import { - calculateDistance, - generateQuery, - validateQueryCriteria, - validateGeoDocument, -} from '../utils'; +import {validateGeoDocument, validateQueryCriteria} from './validate'; +import {GeoFirestoreTypes} from '../definitions'; +import {calculateDistance, generateQuery} from '../utils'; interface DocMap { change: GeoFirestoreTypes.web.DocumentChange; diff --git a/src/api/snapshot.ts b/src/api/snapshot.ts index 8c75f6d..87b0376 100644 --- a/src/api/snapshot.ts +++ b/src/api/snapshot.ts @@ -1,5 +1,6 @@ +import {validateLocation} from './validate'; import {GeoFirestoreTypes} from '../definitions'; -import {generateGeoQueryDocumentSnapshot, validateLocation} from '../utils'; +import {generateGeoQueryDocumentSnapshot} from '../utils'; /** * A `GeoQuerySnapshot` contains zero or more `QueryDocumentSnapshot` objects diff --git a/src/api/validate.ts b/src/api/validate.ts new file mode 100644 index 0000000..6478d02 --- /dev/null +++ b/src/api/validate.ts @@ -0,0 +1,156 @@ +import {validateHash} from 'geokit'; +import {GeoFirestoreTypes} from '../definitions'; + +/** + * Validates a GeoPoint object and returns a boolean if valid, or throws an error if invalid. + * + * @param location The Firestore GeoPoint to be verified. + * @param flag Tells function to send up boolean if not valid instead of throwing an error. + */ +export function validateLocation( + location: GeoFirestoreTypes.web.GeoPoint | GeoFirestoreTypes.cloud.GeoPoint, + flag = false +): boolean { + let error: string; + + if (!location) { + error = 'GeoPoint must exist'; + } else if (typeof location.latitude === 'undefined') { + error = 'latitude must exist on GeoPoint'; + } else if (typeof location.longitude === 'undefined') { + error = 'longitude must exist on GeoPoint'; + } else { + const latitude = location.latitude; + const longitude = location.longitude; + + if (typeof latitude !== 'number' || isNaN(latitude)) { + error = 'latitude must be a number'; + } else if (latitude < -90 || latitude > 90) { + error = 'latitude must be within the range [-90, 90]'; + } else if (typeof longitude !== 'number' || isNaN(longitude)) { + error = 'longitude must be a number'; + } else if (longitude < -180 || longitude > 180) { + error = 'longitude must be within the range [-180, 180]'; + } + } + + if (typeof error !== 'undefined' && !flag) { + throw new Error('Invalid location: ' + error); + } else { + return !error; + } +} + +/** + * Validates the inputted limit and throws an error, or returns boolean, if it is invalid. + * + * @param limit The limit to be applied by `GeoQuery.limit()` + * @param flag Tells function to send up boolean if valid instead of throwing an error. + */ +export function validateLimit(limit: number, flag = false): boolean { + let error: string; + if (typeof limit !== 'number' || isNaN(limit)) { + error = 'limit must be a number'; + } else if (limit < 0) { + error = 'limit must be greater than or equal to 0'; + } + + if (typeof error !== 'undefined' && !flag) { + throw new Error(error); + } else { + return !error; + } +} + +/** + * Validates the inputted GeoDocument object and throws an error, or returns boolean, if it is invalid. + * + * @param documentData The GeoDocument object to be validated. + * @param flag Tells function to send up boolean if valid instead of throwing an error. + * @return Flag if data is valid + */ +export function validateGeoDocument( + documentData: GeoFirestoreTypes.GeoDocumentData, + flag = false +): boolean { + let error: string; + + if (!documentData) { + error = 'no document found'; + } else if ('g' in documentData) { + error = !validateHash(documentData.g.geohash, true) + ? 'invalid geohash on object' + : null; + error = !validateLocation(documentData.g.geopoint, true) + ? 'invalid location on object' + : error; + } else { + error = 'no `g` field found in object'; + } + + if (error && !flag) { + throw new Error('Invalid GeoFirestore object: ' + error); + } else { + return !error; + } +} + +/** + * Validates the inputted query criteria and throws an error if it is invalid. + * + * @param newQueryCriteria The criteria which specifies the query's center and/or radius. + * @param requireCenterAndRadius The criteria which center and radius required. + */ +export function validateQueryCriteria( + newQueryCriteria: GeoFirestoreTypes.QueryCriteria, + requireCenterAndRadius = false +): void { + if (typeof newQueryCriteria !== 'object') { + throw new Error('QueryCriteria must be an object'); + } else if ( + typeof newQueryCriteria.center === 'undefined' && + typeof newQueryCriteria.radius === 'undefined' + ) { + throw new Error('radius and/or center must be specified'); + } else if ( + requireCenterAndRadius && + (typeof newQueryCriteria.center === 'undefined' || + typeof newQueryCriteria.radius === 'undefined') + ) { + throw new Error( + 'QueryCriteria for a new query must contain both a center and a radius' + ); + } + + // Throw an error if there are any extraneous attributes + const keys: string[] = Object.keys(newQueryCriteria); + for (const key of keys) { + if (!['center', 'radius', 'limit'].includes(key)) { + throw new Error( + "Unexpected attribute '" + key + "' found in query criteria" + ); + } + } + + // Validate the 'center' attribute + if (typeof newQueryCriteria.center !== 'undefined') { + validateLocation(newQueryCriteria.center); + } + + // Validate the 'radius' attribute + if (typeof newQueryCriteria.radius !== 'undefined') { + if ( + typeof newQueryCriteria.radius !== 'number' || + isNaN(newQueryCriteria.radius) + ) { + throw new Error('radius must be a number'); + } else if (newQueryCriteria.radius < 0) { + throw new Error('radius must be greater than or equal to 0'); + } + } + + // Validate the 'limit' attribute + if (typeof newQueryCriteria.limit !== 'undefined') { + validateLimit(newQueryCriteria.limit); + } +} diff --git a/src/index.ts b/src/index.ts index 9e602d0..71c1602 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,16 @@ -export {geoQueryGet} from './api/query-get'; -export {geoQueryOnSnapshot} from './api/query-on-snapshot'; -export {GeoQuerySnapshot} from './api/snapshot'; -export * from './definitions'; export { encodeDocumentAdd, encodeDocumentSet, encodeDocumentUpdate, + encodeGeoDocument, +} from './api/encode'; +export {geoQueryGet} from './api/query-get'; +export {geoQueryOnSnapshot} from './api/query-on-snapshot'; +export {GeoQuerySnapshot} from './api/snapshot'; +export { + validateLocation, validateLimit, + validateGeoDocument, validateQueryCriteria, -} from './utils'; +} from './api/validate'; +export * from './definitions'; diff --git a/src/utils.ts b/src/utils.ts index 8bf94af..50ab297 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,6 @@ import {distance as calcDistance, hash, validateHash} from 'geokit'; + +import {validateLocation, validateGeoDocument} from './api/validate'; import {GeoFirestoreTypes} from './definitions'; // Characters used in location geohashes @@ -142,96 +144,6 @@ export function degreesToRadians(degrees: number): number { return (degrees * Math.PI) / 180; } -/** - * Encodes a Firestore Document to be added as a GeoDocument. - * - * @param documentData The document being set. - * @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 encodeDocumentAdd( - documentData: GeoFirestoreTypes.DocumentData, - customKey?: string -): GeoFirestoreTypes.GeoDocumentData { - if (Object.prototype.toString.call(documentData) !== '[object Object]') { - throw new Error('document must be an object'); - } - const geopoint = findGeoPoint(documentData, customKey); - return encodeGeoDocument(geopoint, documentData); -} - -/** - * Encodes a Firestore Document to be set as a GeoDocument. - * - * @param documentData 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 The document encoded as GeoDocument object. - */ -export function encodeDocumentSet( - documentData: GeoFirestoreTypes.DocumentData, - options?: GeoFirestoreTypes.SetOptions -): GeoFirestoreTypes.GeoDocumentData | GeoFirestoreTypes.DocumentData { - if (Object.prototype.toString.call(documentData) !== '[object Object]') { - throw new Error('document must be an object'); - } - const customKey = options && options.customKey; - const geopoint = findGeoPoint( - documentData, - customKey, - options && (options.merge || !!options.mergeFields) - ); - if (geopoint) { - return encodeGeoDocument(geopoint, documentData); - } - return documentData; -} - -/** - * Encodes a Firestore Document to be updated as a GeoDocument. - * - * @param documentData The document being 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 encodeDocumentUpdate( - documentData: GeoFirestoreTypes.UpdateData, - customKey?: string -): GeoFirestoreTypes.UpdateData { - if (Object.prototype.toString.call(documentData) !== '[object Object]') { - throw new Error('document must be an object'); - } - const geopoint = findGeoPoint(documentData, customKey, true); - if (geopoint) { - documentData = encodeGeoDocument(geopoint, documentData); - } - return documentData; -} - -/** - * Encodes a document with a GeoPoint as a GeoDocument. - * - * @param geopoint The location as a Firestore GeoPoint. - * @param documentData Document to encode. - * @return The document encoded as GeoDocument object. - */ -export function encodeGeoDocument( - geopoint: GeoFirestoreTypes.cloud.GeoPoint | GeoFirestoreTypes.web.GeoPoint, - documentData: GeoFirestoreTypes.DocumentData -): GeoFirestoreTypes.GeoDocumentData { - validateLocation(geopoint); - const geohash = hash({ - lat: geopoint.latitude, - lng: geopoint.longitude, - }); - return { - ...documentData, - g: { - geopoint, - geohash, - }, - }; -} - /** * Finds GeoPoint in a document. * @@ -511,160 +423,6 @@ export function toGeoPoint( return fakeGeoPoint; } -/** - * Validates a GeoPoint object and returns a boolean if valid, or throws an error if invalid. - * - * @param location The Firestore GeoPoint to be verified. - * @param flag Tells function to send up boolean if not valid instead of throwing an error. - */ -export function validateLocation( - location: GeoFirestoreTypes.web.GeoPoint | GeoFirestoreTypes.cloud.GeoPoint, - flag = false -): boolean { - let error: string; - - if (!location) { - error = 'GeoPoint must exist'; - } else if (typeof location.latitude === 'undefined') { - error = 'latitude must exist on GeoPoint'; - } else if (typeof location.longitude === 'undefined') { - error = 'longitude must exist on GeoPoint'; - } else { - const latitude = location.latitude; - const longitude = location.longitude; - - if (typeof latitude !== 'number' || isNaN(latitude)) { - error = 'latitude must be a number'; - } else if (latitude < -90 || latitude > 90) { - error = 'latitude must be within the range [-90, 90]'; - } else if (typeof longitude !== 'number' || isNaN(longitude)) { - error = 'longitude must be a number'; - } else if (longitude < -180 || longitude > 180) { - error = 'longitude must be within the range [-180, 180]'; - } - } - - if (typeof error !== 'undefined' && !flag) { - throw new Error('Invalid location: ' + error); - } else { - return !error; - } -} - -/** - * Validates the inputted limit and throws an error, or returns boolean, if it is invalid. - * - * @param limit The limit to be applied by `GeoQuery.limit()` - * @param flag Tells function to send up boolean if valid instead of throwing an error. - */ -export function validateLimit(limit: number, flag = false): boolean { - let error: string; - if (typeof limit !== 'number' || isNaN(limit)) { - error = 'limit must be a number'; - } else if (limit < 0) { - error = 'limit must be greater than or equal to 0'; - } - - if (typeof error !== 'undefined' && !flag) { - throw new Error(error); - } else { - return !error; - } -} - -/** - * Validates the inputted GeoDocument object and throws an error, or returns boolean, if it is invalid. - * - * @param documentData The GeoDocument object to be validated. - * @param flag Tells function to send up boolean if valid instead of throwing an error. - * @return Flag if data is valid - */ -export function validateGeoDocument( - documentData: GeoFirestoreTypes.GeoDocumentData, - flag = false -): boolean { - let error: string; - - if (!documentData) { - error = 'no document found'; - } else if ('g' in documentData) { - error = !validateHash(documentData.g.geohash, true) - ? 'invalid geohash on object' - : null; - error = !validateLocation(documentData.g.geopoint, true) - ? 'invalid location on object' - : error; - } else { - error = 'no `g` field found in object'; - } - - if (error && !flag) { - throw new Error('Invalid GeoFirestore object: ' + error); - } else { - return !error; - } -} - -/** - * Validates the inputted query criteria and throws an error if it is invalid. - * - * @param newQueryCriteria The criteria which specifies the query's center and/or radius. - * @param requireCenterAndRadius The criteria which center and radius required. - */ -export function validateQueryCriteria( - newQueryCriteria: GeoFirestoreTypes.QueryCriteria, - requireCenterAndRadius = false -): void { - if (typeof newQueryCriteria !== 'object') { - throw new Error('QueryCriteria must be an object'); - } else if ( - typeof newQueryCriteria.center === 'undefined' && - typeof newQueryCriteria.radius === 'undefined' - ) { - throw new Error('radius and/or center must be specified'); - } else if ( - requireCenterAndRadius && - (typeof newQueryCriteria.center === 'undefined' || - typeof newQueryCriteria.radius === 'undefined') - ) { - throw new Error( - 'QueryCriteria for a new query must contain both a center and a radius' - ); - } - - // Throw an error if there are any extraneous attributes - const keys: string[] = Object.keys(newQueryCriteria); - for (const key of keys) { - if (!['center', 'radius', 'limit'].includes(key)) { - throw new Error( - "Unexpected attribute '" + key + "' found in query criteria" - ); - } - } - - // Validate the 'center' attribute - if (typeof newQueryCriteria.center !== 'undefined') { - validateLocation(newQueryCriteria.center); - } - - // Validate the 'radius' attribute - if (typeof newQueryCriteria.radius !== 'undefined') { - if ( - typeof newQueryCriteria.radius !== 'number' || - isNaN(newQueryCriteria.radius) - ) { - throw new Error('radius must be a number'); - } else if (newQueryCriteria.radius < 0) { - throw new Error('radius must be greater than or equal to 0'); - } - } - - // Validate the 'limit' attribute - if (typeof newQueryCriteria.limit !== 'undefined') { - validateLimit(newQueryCriteria.limit); - } -} - /** * Wraps the longitude to [-180,180]. *