UNPKG

minimongo

Version:

Client-side mongo database with server sync over http

388 lines (387 loc) 16.1 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.regularizeUpsert = exports.createUid = exports.filterFields = exports.processFind = exports.cloneLocalCollection = exports.cloneLocalDb = exports.migrateLocalDb = exports.autoselectLocalDb = exports.compileDocumentSelector = void 0; // Utilities for db handling const lodash_1 = __importDefault(require("lodash")); const selector_1 = require("./selector"); Object.defineProperty(exports, "compileDocumentSelector", { enumerable: true, get: function () { return selector_1.compileDocumentSelector; } }); const boolean_point_in_polygon_1 = __importDefault(require("@turf/boolean-point-in-polygon")); const intersect_1 = __importDefault(require("@turf/intersect")); const boolean_crosses_1 = __importDefault(require("@turf/boolean-crosses")); const boolean_within_1 = __importDefault(require("@turf/boolean-within")); const IndexedDb_1 = __importDefault(require("./IndexedDb")); const WebSQLDb_1 = __importDefault(require("./WebSQLDb")); const LocalStorageDb_1 = __importDefault(require("./LocalStorageDb")); const MemoryDb_1 = __importDefault(require("./MemoryDb")); const HybridDb_1 = __importDefault(require("./HybridDb")); const distance_1 = __importDefault(require("@turf/distance")); const nearest_point_on_line_1 = __importDefault(require("@turf/nearest-point-on-line")); // Test window.localStorage function isLocalStorageSupported() { if (!window.localStorage) { return false; } try { window.localStorage.setItem("test", "test"); window.localStorage.removeItem("test"); return true; } catch (e) { return false; } } // Select appropriate local database, prefering IndexedDb, then WebSQLDb, then LocalStorageDb, then MemoryDb function autoselectLocalDb(options, success, error) { var _a; // Browsers with no localStorage support don't deserve anything better than a MemoryDb if (!isLocalStorageSupported()) { return new MemoryDb_1.default(options, success); } // Always use WebSQL plugin in cordova iOS only if (window["cordova"]) { if (((_a = window["device"]) === null || _a === void 0 ? void 0 : _a.platform) === "iOS" && window["sqlitePlugin"]) { console.log("Selecting WebSQLDb(sqlite) for Cordova"); options.storage = "sqlite"; return new WebSQLDb_1.default(options, success, error); } } // Always use IndexedDb in browser if supported if (window.indexedDB) { console.log("Selecting IndexedDb for browser"); return new IndexedDb_1.default(options, success, (err) => { console.log("Failed to create IndexedDb: " + (err ? err.message : undefined)); // Create LocalStorageDb instead return new LocalStorageDb_1.default(options, success, (err) => { console.log("Failed to create LocalStorageDb: " + (err ? err.message : undefined)); // Create MemoryDb instead return new MemoryDb_1.default(options, success); }); }); } // Use Local Storage otherwise console.log("Selecting LocalStorageDb for fallback"); return new LocalStorageDb_1.default(options, success, error); } exports.autoselectLocalDb = autoselectLocalDb; // Migrates a local database's pending upserts and removes from one database to another // Useful for upgrading from one type of database to another function migrateLocalDb(fromDb, toDb, success, error) { // Migrate collection using a HybridDb const hybridDb = new HybridDb_1.default(fromDb, toDb); for (let name in fromDb.collections) { const col = fromDb.collections[name]; if (toDb[name]) { hybridDb.addCollection(name); } } return hybridDb.upload(success, error); } exports.migrateLocalDb = migrateLocalDb; function cloneLocalDb(fromDb, toDb, success, error) { if (!success && !error) { return new Promise((resolve, reject) => { cloneLocalDb(fromDb, toDb, resolve, reject); }); } function clone() { return __awaiter(this, void 0, void 0, function* () { // Create collections in toDb for all collections in fromDb for (const name in fromDb.collections) { if (!toDb.collections[name]) { yield new Promise((resolve, reject) => { toDb.addCollection(name, resolve, reject); }); } } // Clone each collection in parallel yield Promise.all(Object.values(fromDb.collections).map((fromCol) => { return cloneLocalCollection(fromCol, toDb.collections[fromCol.name]); })); }); } clone().then(success).catch(error); } exports.cloneLocalDb = cloneLocalDb; function cloneLocalCollection(fromCol, toCol, success, error) { if (!success && !error) { return new Promise((resolve, reject) => { cloneLocalCollection(fromCol, toCol, resolve, reject); }); } function clone() { return __awaiter(this, void 0, void 0, function* () { // Get all items const items = yield fromCol.find({}).fetch(); // Seed items yield new Promise((resolve, reject) => { toCol.seed(items, resolve, reject); }); // Copy upserts const upserts = yield new Promise((resolve, reject) => { fromCol.pendingUpserts(resolve, reject); }); // Upsert items yield toCol.upsert(upserts.map((item) => item.doc), upserts.map((item) => item.base)); // Copy removes const removes = yield new Promise((resolve, reject) => { fromCol.pendingRemoves(resolve, reject); }); // Remove items for (let remove of removes) { yield toCol.remove(remove); } }); } clone().then(success).catch(error); } exports.cloneLocalCollection = cloneLocalCollection; // Processes a find with sorting and filtering and limiting function processFind(items, selector, options) { let filtered = lodash_1.default.filter(items, (0, selector_1.compileDocumentSelector)(selector)); // Handle geospatial operators filtered = processNearOperator(selector, filtered); filtered = processGeoIntersectsOperator(selector, filtered); if (options && options.sort) { filtered.sort((0, selector_1.compileSort)(options.sort)); } if (options && options.skip) { filtered = lodash_1.default.slice(filtered, options.skip); } if (options && options.limit) { filtered = lodash_1.default.take(filtered, options.limit); } // Apply fields if present if (options && options.fields) { filtered = exports.filterFields(filtered, options.fields); } return filtered; } exports.processFind = processFind; /** Include/exclude fields in mongo-style */ function filterFields(items, fields = {}) { // Handle trivial case if (lodash_1.default.keys(fields).length === 0) { return items; } // For each item return lodash_1.default.map(items, function (item) { let field, obj, path, pathElem; const newItem = {}; if (lodash_1.default.first(lodash_1.default.values(fields)) === 1) { // Include fields for (field of lodash_1.default.keys(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 lodash_1.default.initial(path)) { to[pathElem] = to[pathElem] || {}; // Move inside to = to[pathElem]; from = from[pathElem]; } // Copy value to[lodash_1.default.last(path)] = from[lodash_1.default.last(path)]; } return newItem; } else { // Deep clone as we will be deleting keys from item to exclude fields item = JSON.parse(JSON.stringify(item)); // Exclude fields for (field of lodash_1.default.keys(fields)) { path = field.split("."); // Go inside path obj = item; for (pathElem of lodash_1.default.initial(path)) { if (obj) { obj = obj[pathElem]; } } // If not there, don't exclude if (obj == null) { continue; } delete obj[lodash_1.default.last(path)]; } return item; } }); } exports.filterFields = filterFields; // Creates a unique identifier string 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); }); } exports.createUid = createUid; function processNearOperator(selector, list) { for (var key in selector) { var value = selector[key]; if (value != null && value["$near"]) { var geo = value["$near"]["$geometry"]; if (geo.type !== "Point") { break; } // Filter to points and lines list = lodash_1.default.filter(list, (doc) => doc[key] && (doc[key].type === "Point" || doc[key].type === "LineString")); // Get distances let distances = lodash_1.default.map(list, (doc) => ({ doc, distance: getDistance(geo, doc[key]) })); // Filter non-points distances = lodash_1.default.filter(distances, (item) => item.distance >= 0); // Sort by distance distances = lodash_1.default.sortBy(distances, "distance"); // Filter by maxDistance if (value["$near"]["$maxDistance"]) { distances = lodash_1.default.filter(distances, (item) => item.distance <= value["$near"]["$maxDistance"]); } // Extract docs list = lodash_1.default.map(distances, "doc"); } } return list; } function getDistance(from, to) { if (to.type === "Point") { return (0, distance_1.default)(from, to, { units: "meters" }); } if (to.type === "LineString") { const nearest = (0, nearest_point_on_line_1.default)(to, from, { units: "meters" }); return nearest.properties.dist; } throw new Error("Unsupported type"); } function pointInPolygon(point, polygon) { return (0, boolean_point_in_polygon_1.default)(point, polygon); } function polygonIntersection(polygon1, polygon2) { return (0, intersect_1.default)(polygon1, polygon2) != null; } // From http://www.movable-type.co.uk/scripts/latlong.html function getDistanceFromLatLngInM(lat1, lng1, lat2, lng2) { const R = 6370986; // 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) { return deg * (Math.PI / 180); } function processGeoIntersectsOperator(selector, list) { for (var key in selector) { const value = selector[key]; if (value != null && value["$geoIntersects"]) { var geo = value["$geoIntersects"]["$geometry"]; // Can only test intersection with polygon if (geo.type !== "Polygon") { break; } // Check within for each list = lodash_1.default.filter(list, function (doc) { // Ignore if null if (!doc[key]) { return false; } // Check point or polygon if (doc[key].type === "Point") { return pointInPolygon(doc[key], geo); } else if (["Polygon", "MultiPolygon"].includes(doc[key].type)) { return polygonIntersection(doc[key], geo); } else if (doc[key].type === "LineString") { // Special case for empty line string (bug Dec 2023) if (doc[key].coordinates.length === 0) { return false; } return (0, boolean_crosses_1.default)(doc[key], geo) || (0, boolean_within_1.default)(doc[key], geo); } else if (doc[key].type === "MultiLineString") { // Bypass deficiencies in turf.js by splitting it up for (let line of doc[key].coordinates) { const lineGeo = { type: "LineString", coordinates: line }; // Special case for empty line string (bug Dec 2023) if (lineGeo.coordinates.length === 0) { continue; } if ((0, boolean_crosses_1.default)(lineGeo, geo) || (0, boolean_within_1.default)(lineGeo, geo)) { return true; } } return false; } }); } } return list; } /** Tidy up upsert parameters to always be a list of { doc: <doc>, base: <base> }, * doing basic error checking and making sure that _id is present * Returns [items, success, error] */ function regularizeUpsert(docs, bases, success, error) { // Handle case of bases not present if (lodash_1.default.isFunction(bases)) { ; [bases, success, error] = [undefined, bases, success]; } // Handle single upsert if (!lodash_1.default.isArray(docs)) { docs = [docs]; bases = [bases]; } else { bases = bases || []; } // Make into list of { doc: .., base: } const items = lodash_1.default.map(docs, (doc, i) => ({ doc, base: i < bases.length ? bases[i] : undefined })); // Set _id for (let item of items) { if (!item.doc._id) { item.doc._id = exports.createUid(); } if (item.base && !item.base._id) { throw new Error("Base needs _id"); } if (item.base && item.base._id !== item.doc._id) { throw new Error("Base needs same _id"); } } return [items, success, error]; } exports.regularizeUpsert = regularizeUpsert;