UNPKG

minimongo

Version:

Client-side mongo database with server sync over http

374 lines (373 loc) 13.7 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const lodash_1 = __importDefault(require("lodash")); const utils = __importStar(require("./utils")); const jQueryHttpClient_1 = __importDefault(require("./jQueryHttpClient")); const quickfind = __importStar(require("./quickfind")); const js_sha1_1 = __importDefault(require("js-sha1")); class RemoteDb { /** Url must have trailing /, can be an arrau of URLs * useQuickFind enables the quickfind protocol for finds * usePostFind enables POST for find */ constructor(url, client, httpClient, useQuickFind = false, usePostFind = false) { this.url = url; this.client = client; this.collections = {}; this.httpClient = httpClient; this.useQuickFind = useQuickFind; this.usePostFind = usePostFind; } // Can specify url of specific collection as option. // useQuickFind can be overridden in options // usePostFind can be overridden in options addCollection(name, options = {}, success, error) { let url; if (lodash_1.default.isFunction(options)) { ; [options, success, error] = [{}, options, success]; } if (options.url) { ; ({ url } = options); } else { if (lodash_1.default.isArray(this.url)) { url = lodash_1.default.map(this.url, (url) => url + name); } else { url = this.url + name; } } let { useQuickFind } = this; if (options.useQuickFind != null) { ; ({ useQuickFind } = options); } let { usePostFind } = this; if (options.usePostFind != null) { ; ({ usePostFind } = options); } const collection = new Collection(name, url, this.client, this.httpClient, useQuickFind, usePostFind); this[name] = collection; this.collections[name] = collection; if (success != null) { return success(); } } removeCollection(name, success, error) { delete this[name]; delete this.collections[name]; if (success != null) { return success(); } } getCollectionNames() { return lodash_1.default.keys(this.collections); } } exports.default = RemoteDb; // Remote collection on server class Collection { // usePostFind allows POST to <collection>/find for long selectors constructor(name, url, client, httpClient, useQuickFind, usePostFind) { this.name = name; this.url = url; this.client = client; this.httpClient = httpClient || jQueryHttpClient_1.default; this.useQuickFind = useQuickFind; this.usePostFind = usePostFind; this.urlIndex = 0; } /** Get a URL to use from an array, if present. * Stable if a GET request, but not if a POST, etc request * to allow caching. Accomplished by passing in a key * to use for hashing for GET only. */ getUrl(key) { if (typeof this.url === "string") { return this.url; } // If no key, use next URL if (!key) { const url = this.url[this.urlIndex]; this.urlIndex = (this.urlIndex + 1) % this.url.length; return url; } // Hash key to get index const hash = js_sha1_1.default.create(); hash.update(key); // Get 4 hex digits const partial = hash.hex().substr(0, 4); // Convert to integer const index = parseInt(partial, 16); // Get URL return this.url[index % this.url.length]; } // error is called with jqXHR find(selector, options = {}) { return { fetch: (success, error) => { return this._findFetch(selector, options, success, error); } }; } _findFetch(selector, options, success, error) { // If promise case if (success == null) { return new Promise((resolve, reject) => { this._findFetch(selector, options, resolve, reject); }); } // Determine method: "get", "post" or "quickfind" // If in quickfind and localData present and (no fields option or _rev included) and not (limit with no sort), use quickfind let method; if (this.useQuickFind && options.localData && (!options.fields || options.fields._rev) && !(options.limit && !options.sort && !options.orderByExprs)) { method = "quickfind"; // If selector or fields or sort is too big, use post } else if (this.usePostFind && JSON.stringify({ selector, sort: options.sort, fields: options.fields }).length > 500) { method = "post"; } else { method = "get"; } if (method === "get") { // Create url const params = {}; params.selector = JSON.stringify(selector || {}); if (options.sort) { params.sort = JSON.stringify(options.sort); } if (options.limit) { params.limit = options.limit; } if (options.skip) { params.skip = options.skip; } if (options.fields) { params.fields = JSON.stringify(options.fields); } // Advanced options for mwater-expression-based filtering and ordering if (options.whereExpr) { params.whereExpr = JSON.stringify(options.whereExpr); } if (options.orderByExprs) { params.orderByExprs = JSON.stringify(options.orderByExprs); } if (this.client) { params.client = this.client; } this.httpClient("GET", this.getUrl(this.name + JSON.stringify(params)), params, null, success, error); return; } // Create body + params for quickfind and post const body = { selector: selector || {} }; if (options.sort) { body.sort = options.sort; } if (options.limit != null) { body.limit = options.limit; } if (options.skip != null) { body.skip = options.skip; } if (options.fields) { body.fields = options.fields; } // Advanced options for mwater-expression-based filtering and ordering if (options.whereExpr) { body.whereExpr = options.whereExpr; } if (options.orderByExprs) { body.orderByExprs = options.orderByExprs; } const params = {}; if (this.client) { params.client = this.client; } if (method === "quickfind") { // Send quickfind data body.quickfind = quickfind.encodeRequest(options.localData); this.httpClient("POST", this.getUrl() + "/quickfind", params, body, (encodedResponse) => { return success(quickfind.decodeResponse(encodedResponse, options.localData, options.sort)); }, error); return; } // POST method this.httpClient("POST", this.getUrl() + "/find", params, body, (response) => { return success(response); }, error); return; } findOne(selector, options, success, error) { if (lodash_1.default.isFunction(options)) { ; [options, success, error] = [{}, options, success]; } options = options || {}; // If promise case if (success == null) { return new Promise((resolve, reject) => { this.findOne(selector, options, resolve, reject); }); } // TODO would require documentation change and major bump // // Use simple GET if no options and selector is by _id only // if (_.isEmpty(options) && _.isEqual(selector, { _id: selector._id })) { // return this.httpClient( // "GET", // this.getUrl(this.name + "/" + selector._id), // { client: this.client }, // null, // function (result: any) { // success(result ?? null) // }, // (jqXHR) => { // if (jqXHR.status === 404) { // return success(null) // } else { // return error(jqXHR) // } // } // ) // } // Create url const params = {}; if (options.sort) { params.sort = JSON.stringify(options.sort); } params.limit = 1; if (this.client) { params.client = this.client; } params.selector = JSON.stringify(selector || {}); return this.httpClient("GET", this.getUrl(this.name + "?" + JSON.stringify(params)), params, null, function (results) { if (results && results.length > 0) { return success(results[0]); } else { return success(null); } }, error); } upsert(docs, bases, success, error) { // If promise case if (!success && !lodash_1.default.isFunction(bases)) { return new Promise((resolve, reject) => { this.upsert(docs, bases, resolve, reject); }); } let items; [items, success, error] = utils.regularizeUpsert(docs, bases, success, error); const results = []; // Check if bases present const basesPresent = lodash_1.default.compact(lodash_1.default.map(items, "base")).length > 0; const params = {}; if (this.client) { params.client = this.client; } // Handle single case if (items.length === 1) { // POST if no base, PATCH otherwise if (basesPresent) { return this.httpClient("PATCH", this.getUrl(), params, items[0], function (result) { if (lodash_1.default.isArray(docs)) { return success([result]); } else { return success(result); } }, function (err) { if (error) { return error(err); } }); } else { return this.httpClient("POST", this.getUrl(), params, items[0].doc, function (result) { if (lodash_1.default.isArray(docs)) { return success([result]); } else { return success(result); } }, function (err) { if (error) { return error(err); } }); } } else { // POST if no base, PATCH otherwise if (basesPresent) { return this.httpClient("PATCH", this.getUrl(), params, { doc: lodash_1.default.map(items, "doc"), base: lodash_1.default.map(items, "base") }, (result) => success(result), function (err) { if (error) { return error(err); } }); } else { return this.httpClient("POST", this.getUrl(), params, lodash_1.default.map(items, "doc"), (result) => success(result), function (err) { if (error) { return error(err); } }); } } } remove(id, success, error) { if (!success) { return new Promise((resolve, reject) => { this.remove(id, resolve, reject); }); } if (!this.client) { throw new Error("Client required to remove"); } const params = { client: this.client }; return this.httpClient("DELETE", this.getUrl() + "/" + id, params, null, success, function (err) { // 410 is an acceptable delete status if (err.status === 410) { return success(); } else { return error(err); } }); } }