UNPKG

mongo-portable

Version:

Portable Pure JS MongoDB - Based on Monglodb (https://github.com/euforic/monglodb.git) by Christian Sullivan (http://RogueSynaptics.com)

689 lines (585 loc) 15.9 kB
import { JSWLogger } from "jsw-logger"; import * as _ from "lodash"; import { Selector } from "../selector"; /* class Options { public skip: number; public limit: number; public sort; private __defaultOptions = { skip: 0, limit: 15, sort: null }; constructor(options?: any) { if (_.isNil(options)) { options = {}; } this.skip = (options.skip ? options.skip : this.__defaultOptions.skip); this.limit = (options.limit ? options.limit : this.__defaultOptions.limit); this.sort = (options.sort ? options.sort : this.__defaultOptions.sort); } } */ /*** * Cursor * * @module Cursor * @since 0.0.1 * @author Eduardo Astolfi <eduardo.astolfi91@gmail.com> * @copyright 2016 Eduardo Astolfi <eduardo.astolfi91@gmail.com> * @license MIT Licensed * @classdesc Cursor class that maps a MongoDB-like cursor */ export class Cursor { public static COLSCAN = "colscan"; public static IDXSCAN = "idxscan"; public documents; public selector; public fields; public skipValue; public limitValue; public sortValue; public sorted: boolean = false; public selectorCompiled; public selectorId; public fetchMode; public indexes = null; public sortCompiled; public dbObjects; public cursorPosition; protected logger: JSWLogger; private defaultOptions = { skip: 0, limit: 15, sort: null }; /*** * @param {MongoPortable} db - Additional options * @param {Array} documents - The list of documents * @param {Object|Array|String} [selection={}] - The selection for matching documents * @param {Object|Array|String} [fields={}] - The fields of the document to show * @param {Object} [options] - Database object * * @param {Object} [options.pkFactory=null] - Object overriding the basic "ObjectId" primary key generation. */ constructor(documents, selection, fields?, options: object = {}) { this.documents = documents; this.selector = selection; const opts = _.assign({}, this.defaultOptions, options); this.skipValue = opts.skip; this.limitValue = opts.limit; this.sortValue = opts.sort; this.logger = JSWLogger.instance; /**** ADD IDX ****/ if (Selector.isSelectorCompiled(this.selector)) { this.selectorCompiled = this.selector; } else { this.selectorCompiled = new Selector(this.selector, Selector.MATCH_SELECTOR); } for (const clause of this.selectorCompiled.clauses) { if (clause.key === "_id") { this.selectorId = clause.value; } } for (const clause of this.selectorCompiled.clauses) { if (clause.key === "_id") { const val = clause.value; if (_.isString(val) || _.isNumber(val)) { this.selectorId = val; } } } /**** ADD IDX ****/ this.fetchMode = Cursor.COLSCAN || Cursor.IDXSCAN; // this.indexes = null;//findUsableIndexes(); // if (cursor.fetchMode === Cursor.COLSCAN) { // // COLSCAN, wi will iterate over all documents // docs = _.cloneDeep(cursor.collection.docs); // } else if (cursor.fetchMode === Cursor.IDXSCAN) { // // IDXSCAN, wi will iterate over all needed documents // for (let i = 0; i < cursor.indexes.length; i++) { // let index = cursor.indexes[i]; // for (let i = index.start; i < index.end; i++) { // let idx_id = cursor.collection.getIndex(index.name)[i]; // docs.push(cursor.collection.docs[idx_id]); // } // } // } this.fields = new Selector(fields, Selector.FIELD_SELECTOR); this.sortCompiled = new Selector(this.sortValue, Selector.SORT_SELECTOR); this.dbObjects = null; this.cursorPosition = 0; } /*** * Moves a cursor to the begining * * @method Cursor#rewind */ public rewind() { this.dbObjects = null; this.cursorPosition = 0; } /*** * Iterates over the cursor, calling a callback function * * @method Cursor#forEach * * @param {Function} [callback=null] - Callback function to be called for each document */ public forEach(callback) { const docs = this.fetchAll(); for (const doc of docs) { callback(doc); } } /*** * Iterates over the cursor, returning a new array with the documents affected by the callback function * * @method Cursor#map * * @param {Function} [callback=null] - Callback function to be called for each document * * @returns {Array} The documents after being affected with the callback function */ public map(callback) { const res = []; this.forEach((doc) => { res.push(callback(doc)); }); return res; } /*** * Checks if the cursor has one document to be fetched * * @method Cursor#hasNext * * @returns {Boolean} True if we can fetch one more document */ public hasNext() { return (this.cursorPosition < this.documents.length); } /*** * Alias for {@link Cursor#fetchOne} * * @method Cursor#next */ public next() { return this.fetchOne(); } /*** * Alias for {@link Cursor#fetchAll} * * @method Cursor#fetch */ public fetch() { return this.fetchAll(); } /*** * Fetch all documents in the cursor * * @method Cursor#fetchAll * * @returns {Array} All the documents contained in the cursor */ public fetchAll() { return getDocuments(this, false) || []; } /*** * Retrieves the next document in the cursor * * @method Cursor#fetchOne * * @returns {Object} The next document in the cursor */ public fetchOne() { return getDocuments(this, true); } /*** * Obtains the total of documents of the cursor * * @method Cursor#count * * @returns {Number} The total of documents in the cursor */ public count() { return this.fetchAll().length; } /*** * Set the sorting of the cursor * * @method Cursor#sort * * @param {Object|Array|String} spec - The sorting specification * * @returns {Cursor} This instance so it can be chained with other methods */ public setSorting(spec) { if (_.isNil(spec)) { this.logger.throw("You need to specify a sorting"); } if (spec) { this.sortValue = spec; this.sortCompiled = (new Selector(spec, Selector.SORT_SELECTOR)); } return this; } /*** * Applies a sorting on the cursor * * @method Cursor#sort * * @param {Object|Array|String} spec - The sorting specification * * @returns {Cursor} This instance so it can be chained with other methods */ public sort(spec) { let _sort = this.sortCompiled || null; if (spec) { _sort = new Selector(spec, Selector.SORT_SELECTOR); } if (_sort) { if (!_.isNil(this.dbObjects) && _.isArray(this.dbObjects)) { this.dbObjects = this.dbObjects.sort(_sort); this.sorted = true; } else { this.setSorting(spec); } } return this; } /*** * Set the number of document to skip when fetching the cursor * * @method Cursor#skip * * @param {Number} skip - The number of documents to skip * * @returns {Cursor} This instance so it can be chained with other methods */ public skip(skip) { if (_.isNil(skip) || _.isNaN(skip)) { throw new Error("Must pass a number"); } this.skipValue = skip; return this; } /*** * Set the max number of document to fetch * * @method Cursor#limit * * @param {Number} limit - The max number of documents * * @returns {Cursor} This instance so it can be chained with other methods */ public limit(limit) { if (_.isNil(limit) || _.isNaN(limit)) { throw new Error("Must pass a number"); } this.limitValue = limit; return this; } /*** * @todo Implement */ public batchSize() { // Controls the number of documents MongoDB will return to the client in a single network message. throw new Error("Not yet implemented"); } /*** * @todo Implement */ public close() { // Close a cursor and free associated server resources. throw new Error("Not yet implemented"); } /*** * @todo Implement */ public comment() { // Attaches a comment to the query to allow for traceability in the logs and the system.profile collection. throw new Error("Not yet implemented"); } /*** * @todo Implement */ public explain() { // Reports on the query execution plan for a cursor. throw new Error("Not yet implemented"); } /*** * @todo Implement */ public hint() { // Forces MongoDB to use a specific index for a query. throw new Error("Not yet implemented"); } /*** * @todo Implement */ public itcount() { // Computes the total number of documents in the cursor client-side by fetching and iterating the result set. throw new Error("Not yet implemented"); } /*** * @todo Implement */ public maxScan() { // Specifies the maximum number of items to scan; documents for collection scans, keys for index scans. throw new Error("Not yet implemented"); } /*** * @todo Implement */ public maxTimeMS() { // Specifies a cumulative time limit in milliseconds for processing operations on a cursor. throw new Error("Not yet implemented"); } /*** * @todo Implement */ public max() { // Specifies an exclusive upper index bound for a cursor. For use with cursor.hint() throw new Error("Not yet implemented"); } /*** * @todo Implement */ public min() { // Specifies an inclusive lower index bound for a cursor. For use with cursor.hint() throw new Error("Not yet implemented"); } /*** * @todo Implement */ public noCursorTimeout() { // Instructs the server to avoid closing a cursor automatically after a period of inactivity. throw new Error("Not yet implemented"); } /*** * @todo Implement */ public objsLeftInBatch() { // Returns the number of documents left in the current cursor batch. throw new Error("Not yet implemented"); } /*** * @todo Implement */ public pretty() { // Configures the cursor to display results in an easy-to-read format. throw new Error("Not yet implemented"); } /*** * @todo Implement */ public readConcern() { // Specifies a read concern for a find() operation. throw new Error("Not yet implemented"); } /*** * @todo Implement */ public readPref() { // Specifies a read preference to a cursor to control how the client directs queries to a replica set. throw new Error("Not yet implemented"); } /*** * @todo Implement */ public returnKey() { // Modifies the cursor to return index keys rather than the documents. throw new Error("Not yet implemented"); } /*** * @todo Implement */ public showRecordId() { // Adds an internal storage engine ID field to each document returned by the cursor. throw new Error("Not yet implemented"); } /*** * @todo Implement */ public size() { // Returns a count of the documents in the cursor after applying skip() and limit() methods. throw new Error("Not yet implemented"); } /*** * @todo Implement */ public snapshot() { // Forces the cursor to use the index on the _id field. Ensures that the cursor returns each document, // with regards to the value of the _id field, only once. throw new Error("Not yet implemented"); } /*** * @todo Implement */ public tailable() { // Marks the cursor as tailable. Only valid for cursors over capped collections. throw new Error("Not yet implemented"); } /*** * @todo Implement */ public toArray() { // Returns an array that contains all documents returned by the cursor. throw new Error("Not yet implemented"); } public static sort(doc, fields) { // Sort the elements of a cursor throw new Error("Not yet implemented"); } /*** * Projects the fields of one or several documents, changing the output * * @method Cursor.project * * @param {Array|Object} doc - The document/s that will be projected * @param {String|Array|Object} spec - Fields projection specification. Can be an space/comma separated list, an array, or an object * * @returns {Array|Object} The document/s after the projection */ public static project(doc, spec, aggregation = false) { // if (_.isNil(doc)) this.logger.throw("doc param required"); // if (_.isNil(spec)) this.logger.throw("spec param required"); let fields = null; if (aggregation) { fields = new Selector(spec, Selector.AGG_FIELD_SELECTOR); } else { fields = new Selector(spec, Selector.FIELD_SELECTOR); } if (_.isArray(doc)) { for (let i = 0; i < doc.length; i++) { doc[i] = mapFields(doc[i], fields); } return doc; } else { return mapFields(doc, fields); } } } const mapFields = (doc, fields) => { let _doc = _.cloneDeep(doc); if (!_.isNil(fields) && _.isPlainObject(fields) && !_.isEqual(fields, {})) { let showId = true; let showing = null; // Whether if we showing the _id field if (_.hasIn(fields, "_id") && fields._id === -1) { showId = false; } for (const field in fields) { // Whether if we are showing or hidding fields if (field !== "_id") { if (fields[field] === 1) { showing = true; break; } else if (fields[field] === -1) { showing = false; break; } } } let tmp = null; for (const field of Object.keys(fields)) { if (tmp === null) { if (showing) { tmp = {}; } else { tmp = _.cloneDeep(doc); } } // Add or remove the field if (fields[field] === 1 || fields[field] === -1) { // Show the field if (showing) { tmp[field] = doc[field]; } else { // Hide the field delete tmp[field]; } } else { // Show the new field (rename) tmp[field] = doc[fields[field]]; } } // Add or remove the _id field if (showId) { tmp._id = doc._id; } else { delete tmp._id; } _doc = tmp; } return _doc; }; /*** * Retrieves one or all the documents in the cursor * * @method getDocuments * @private * * @param {Cursor} cursor - The cursor with the documents * @param {Boolean} [justOne=false] - Whether it retrieves one or all the documents * * @returns {Array|Object} If [justOne=true] returns the next document, otherwise returns all the documents */ const getDocuments = (cursor, justOne = false) => { let docs = []; if (cursor.fetchMode === Cursor.COLSCAN) { // COLSCAN, wi will iterate over all documents docs = _.cloneDeep(cursor.documents); } else if (cursor.fetchMode === Cursor.IDXSCAN) { // IDXSCAN, wi will iterate over all needed documents for (const index of cursor) { for (let i = index.start; i < index.end; i++) { // let idxId = cursor.collection.getIndex(index.name)[i]; const idxId = index.index[i]; docs.push(cursor.documents[idxId]); } } } // if (cursor.selectorId) { // if (_.hasIn(cursor.collection.doc_indexes, _.toString(cursor.selectorId))) { // let idx = cursor.collection.doc_indexes[_.toString(cursor.selectorId)]; // return Cursor.project(cursor.collection.docs[idx], cursor.fields); // } else { // if (justOne) { // return null; // } else { // return []; // } // } // } // TODO add warning when sort/skip/limit and fetching one // TODO add warning when skip/limit without order // TODO index while (cursor.cursorPosition < docs.length) { let _doc = docs[cursor.cursorPosition]; cursor.cursorPosition++; if (cursor.selectorCompiled.test(_doc)) { if (_.isNil(cursor.dbObjects)) { cursor.dbObjects = []; } _doc = Cursor.project(_doc, cursor.fields); cursor.dbObjects.push(_doc); if (justOne) { // Add force sort return _doc; } } } if (_.isNil(cursor.dbObjects)) { return null; } if (!cursor.sorted && hasSorting(cursor)) { cursor.sort(); } const idxFrom = cursor.skipValue; const idxTo = cursor.limitValue !== -1 ? (cursor.limitValue + idxFrom) : cursor.dbObjects.length; return cursor.dbObjects.slice(idxFrom, idxTo); }; /*** * Checks if a cursor has a sorting defined * * @method hasSorting * @private * * @param {Cursor} cursor - The cursor * * @returns {Boolean} Whether the cursor has sorting or not */ const hasSorting = (cursor) => { if (_.isNil(cursor.sortValue)) { return false; } return true; };