minimongo
Version:
Client-side mongo database with server sync over http
291 lines (245 loc) • 7.92 kB
text/typescript
// TODO: This file was created by bulk-decaffeinate.
// Sanity-check the conversion and remove this comment.
// Utilities for db handling
import _ from "lodash"
import async from "async"
import bowser from "bowser"
import { compileDocumentSelector, compileSort } from "./selector"
// Select appropriate local database, prefering IndexedDb, then WebSQLDb, then LocalStorageDb, then MemoryDb
export function autoselectLocalDb(options: any, success: any, error: any) {
// Here due to browserify circularity quirks
const IndexedDb = require("./IndexedDb")
const WebSQLDb = require("./WebSQLDb")
const LocalStorageDb = require("./LocalStorageDb")
const MemoryDb = require("./MemoryDb")
// Get browser capabilities
const { browser } = bowser
// Always use WebSQL in cordova
if (window.cordova) {
console.log("Selecting WebSQLDb for Cordova")
return new WebSQLDb(options, success, error)
}
// Use WebSQL in Android, iOS, Chrome, Safari, Opera, Blackberry
if (browser.android || browser.ios || browser.chrome || browser.safari || browser.opera || browser.blackberry) {
console.log("Selecting WebSQLDb for browser")
return new WebSQLDb(options, success, error)
}
// Use IndexedDb on Firefox >= 16
if (browser.firefox && browser.version >= 16) {
console.log("Selecting IndexedDb for browser")
return new IndexedDb(options, success, error)
}
// Use Local Storage otherwise
console.log("Selecting LocalStorageDb for fallback")
return new LocalStorageDb(options, success, error)
}
// Migrates a local database's pending upserts and removes from one database to another
// Useful for upgrading from one type of database to another
export function migrateLocalDb(fromDb: any, toDb: any, success: any, error: any) {
// Migrate collection using a HybridDb
// Here due to browserify circularity quirks
const HybridDb = require("./HybridDb")
const hybridDb = new HybridDb(fromDb, toDb)
for (let name in fromDb.collections) {
const col = fromDb.collections[name]
if (toDb[name]) {
hybridDb.addCollection(name)
}
}
return hybridDb.upload(success, error)
}
// Processes a find with sorting and filtering and limiting
export function processFind(items: any, selector: any, options: any) {
let filtered = _.filter(_.values(items), compileDocumentSelector(selector))
// Handle geospatial operators
filtered = processNearOperator(selector, filtered)
filtered = processGeoIntersectsOperator(selector, filtered)
if (options && options.sort) {
filtered.sort(compileSort(options.sort))
}
if (options && options.limit) {
filtered = _.first(filtered, options.limit)
}
// Clone to prevent accidental updates, or apply fields if present
if (options && options.fields) {
// For each item
filtered = _.map(filtered, function (item: any) {
let field, obj, path, pathElem
item = _.cloneDeep(item)
const newItem = {}
if (_.first(_.values(options.fields)) === 1) {
// Include fields
for (field of _.keys(options.fields).concat(["_id"])) {
path = field.split(".")
// Determine if path exists
obj = item
for (pathElem of path) {
if (obj) {
obj = obj[pathElem]
}
}
if (obj == null) {
continue
}
// Go into path, creating as necessary
let from = item
let to = newItem
for (pathElem of _.initial(path)) {
to[pathElem] = to[pathElem] || {}
// Move inside
to = to[pathElem]
from = from[pathElem]
}
// Copy value
to[_.last(path)] = from[_.last(path)]
}
return newItem
} else {
// Exclude fields
for (field of _.keys(options.fields).concat(["_id"])) {
path = field.split(".")
// Go inside path
obj = item
for (pathElem of _.initial(path)) {
if (obj) {
obj = obj[pathElem]
}
}
// If not there, don't exclude
if (obj == null) {
continue
}
delete obj[_.last(path)]
}
return item
}
})
} else {
filtered = _.map(filtered, (doc: any) => _.cloneDeep(doc))
}
return filtered
}
// Creates a unique identifier string
export function createUid() {
return "xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx".replace(/[xy]/g, function (c) {
const r = (Math.random() * 16) | 0
const v = c === "x" ? r : (r & 0x3) | 0x8
return v.toString(16)
})
}
function processNearOperator(selector: any, list: any) {
for (var key in selector) {
var value = selector[key]
if (value != null && value["$near"]) {
var geo = value["$near"]["$geometry"]
if (geo.type !== "Point") {
break
}
list = _.filter(list, (doc: any) => doc[key] && doc[key].type === "Point")
// Get distances
let distances = _.map(list, (doc: any) => ({
doc,
distance: getDistanceFromLatLngInM(
geo.coordinates[1],
geo.coordinates[0],
doc[key].coordinates[1],
doc[key].coordinates[0]
)
}))
// Filter non-points
distances = _.filter(distances, (item: any) => item.distance >= 0)
// Sort by distance
distances = _.sortBy(distances, "distance")
// Filter by maxDistance
if (value["$near"]["$maxDistance"]) {
distances = _.filter(distances, (item: any) => item.distance <= value["$near"]["$maxDistance"])
}
// Limit to 100
distances = _.first(distances, 100)
// Extract docs
list = _.map(distances, "doc")
}
}
return list
}
// Very simple polygon check. Assumes that is a square
function pointInPolygon(point: any, polygon: any) {
// Check that first == last
if (!_.isEqual(_.first(polygon.coordinates[0]), _.last(polygon.coordinates[0]))) {
throw new Error("First must equal last")
}
// Check bounds
if (
point.coordinates[0] <
Math.min.apply(
this,
_.map(polygon.coordinates[0], (coord: any) => coord[0])
)
) {
return false
}
if (
point.coordinates[1] <
Math.min.apply(
this,
_.map(polygon.coordinates[0], (coord: any) => coord[1])
)
) {
return false
}
if (
point.coordinates[0] >
Math.max.apply(
this,
_.map(polygon.coordinates[0], (coord: any) => coord[0])
)
) {
return false
}
if (
point.coordinates[1] >
Math.max.apply(
this,
_.map(polygon.coordinates[0], (coord: any) => coord[1])
)
) {
return false
}
return true
}
// From http://www.movable-type.co.uk/scripts/latlong.html
function getDistanceFromLatLngInM(lat1: any, lng1: any, lat2: any, lng2: any) {
const R = 6371000 // Radius of the earth in m
const dLat = deg2rad(lat2 - lat1) // deg2rad below
const dLng = deg2rad(lng2 - lng1)
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2)
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
const d = R * c // Distance in m
return d
}
function deg2rad(deg: any) {
return deg * (Math.PI / 180)
}
function processGeoIntersectsOperator(selector: any, list: any) {
for (var key in selector) {
const value = selector[key]
if (value != null && value["$geoIntersects"]) {
var geo = value["$geoIntersects"]["$geometry"]
if (geo.type !== "Polygon") {
break
}
// Check within for each
list = _.filter(list, function (doc: any) {
// Reject non-points
if (!doc[key] || doc[key].type !== "Point") {
return false
}
// Check polygon
return pointInPolygon(doc[key], geo)
})
}
}
return list
}