UNPKG

temporalize-documentdb

Version:

Temporalize pluggable storage engine for Microsoft's Azure DocumentDB

854 lines (798 loc) 35.6 kB
// Generated by CoffeeScript 1.10.0 (function() { var HashRing, StorageEngine, WrappedClient, _, async, crypto, getDocLink, getGUID, getLink, path, ref, sqlFromMongo, utils, zxcvbn, bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; utils = require('lumenize').utils; HashRing = require('hashring'); crypto = require('crypto'); zxcvbn = require('zxcvbn'); path = require('path'); ref = require('documentdb-utils'), WrappedClient = ref.WrappedClient, getLink = ref.getLink, getDocLink = ref.getDocLink, _ = ref._, async = ref.async, getGUID = ref.getGUID, sqlFromMongo = ref.sqlFromMongo; module.exports = StorageEngine = (function() { /* */ var _delay; function StorageEngine(userConfig, callback) { this._getSession = bind(this._getSession, this); this.logout = bind(this.logout, this); this.login = bind(this.login, this); this.upsertUser = bind(this.upsertUser, this); this._upsertOne = bind(this._upsertOne, this); this._upsert = bind(this._upsert, this); this.upsert = bind(this.upsert, this); this._writeConfig = bind(this._writeConfig, this); this._readOrCreateInitialConfig = bind(this._readOrCreateInitialConfig, this); this._purgeSessionsContinuously = bind(this._purgeSessionsContinuously, this); this._readConfigContinuously = bind(this._readConfigContinuously, this); this._readConfig = bind(this._readConfig, this); /* */ var config; if (userConfig == null) { userConfig = {}; } config = JSON.parse(JSON.stringify(userConfig)); this.topLevelPartitionField = config.topLevelPartitionField || '_TenantID'; this.secondLevelPartitionField = config.secondLevelPartitionField || '_EntityID'; this.firstTopLevelID = config.firstTopLevelID || 'first'; this.firstSecondLevelID = config.firstSecondLevelID || '0'; this.temporalPolicy = config.temporalPolicy || 'VALID_TIME'; this.refreshConfigMS = config.refreshConfigMS || 10000; this.debug = config.debug || false; this.terminate = config.terminate || false; this.readConfigContinuouslyEventHandler = config.readConfigContinuouslyEventHandler || null; this.cacheSelfLinks = config.cacheSelfLinks || false; this.sessionTTL = config.sessionTTL || 30 * 60 * 1000; this.ongoingTransactions = {}; this.secondLevelHashring = null; this.linkCache = {}; this.lastTimeConfigWasRead = new Date(); this.sessionCacheByID = {}; this.sessionCacheByUsername = {}; this._purgeSessionsContinuously(); this.HIGHEST_DATE_STRING = '9999-01-01T00:00:00.000Z'; this.LOWEST_DATE_STRING = '0001-01-01T00:00:00.000Z'; this.SYSTEM_FIELDS = ["id", "_ValidFrom", "_ValidTo", "_EntityID", "_TenantID", "_CreationTransactionID", "_UpdateTransactionID", "_PreviousValues"]; this.readConfigRetries = 0; if (config.wrappedClient != null) { this.client = wrappedClient; } else { if (config.client != null) { this._client = config.client; new WrappedClient(this._client); } else { this.client = new WrappedClient(); } } this.client.createDatabase({ id: this.firstTopLevelID }, (function(_this) { return function(err, response) { var databaseIDLink, databaseLink; if ((err != null) && err.code !== 409) { throw new Error(JSON.stringify(err)); } else { _this._debug("Created Database. Response:", response); databaseIDLink = getLink(_this.firstTopLevelID); _this.linkCache[databaseIDLink] = response._self; databaseLink = response._self; return _this.client.createCollection(databaseLink, { id: _this.firstSecondLevelID }, function(err, response) { var collectionIDLink; if ((err != null) && err.code !== 409) { throw new Error(JSON.stringify(err)); } else { _this._debug("Created Collection. Response:", response); collectionIDLink = getLink(_this.firstTopLevelID, _this.firstSecondLevelID); _this.linkCache[collectionIDLink] = response._self; return _this._readOrCreateInitialConfig(callback); } }); } }; })(this)); } _delay = function(ms, func) { return setTimeout(func, ms); }; StorageEngine.prototype._debug = function(message, content, content2) { if (this.debug) { console.log(message); if (content != null) { console.log(JSON.stringify(content, null, 2)); console.log(); } if (content2 != null) { console.log(JSON.stringify(content2, null, 2)); return console.log(); } } }; StorageEngine.prototype._handleIfError = function(err, callback) { if (err != null) { this._debug('ERROR', err); if (callback != null) { callback(err); } } else { return false; } }; StorageEngine.prototype._readConfig = function(callback) { return this.client.readDocument(getDocLink(this.firstTopLevelID, this.firstSecondLevelID, 'storage-engine-config'), (function(_this) { return function(err, response, header) { if (err != null) { return callback(err); } else { _this.storageEngineConfig = response; _this.readConfigRetries = 0; _this.lastTimeConfigWasRead = new Date(); _this._debug('Successfully retrieved @storageEngineConfig', _this.storageEngineConfig, header); return callback(_this.storageEngineConfig); } }; })(this)); }; StorageEngine.prototype._readConfigContinuously = function() { if (!this.terminate) { if ((new Date() - this.lastTimeConfigWasRead) > this.refreshConfigMS) { return this._readConfig((function(_this) { return function(nullOrConfig) { setTimeout(_this._readConfigContinuously, _this.refreshConfigMS); if (nullOrConfig != null) { if (_this.readConfigContinuouslyEventHandler != null) { return _this.readConfigContinuouslyEventHandler(nullOrConfig); } } }; })(this)); } else { return setTimeout(this._readConfigContinuously, this.refreshConfigMS); } } }; StorageEngine.prototype._purgeSessionsContinuously = function() { var id, ref1, session, sessionTTL; if (!this.terminate) { ref1 = this.sessionCacheByID; for (id in ref1) { session = ref1[id]; sessionTTL = session.user.sessionTTL || this.sessionTTL; if (new Date() - new Date(session._Created) >= sessionTTL) { delete this.sessionCacheByID[id]; delete this.sessionCacheByUsername[session.user.username]; } } return setTimeout(this._purgeSessionsContinuously, this.refreshConfigMS); } }; StorageEngine.prototype._readOrCreateInitialConfig = function(callback) { return this.client.readDocument(getDocLink(this.firstTopLevelID, this.firstSecondLevelID, 'storage-engine-config'), (function(_this) { return function(err, response, header) { var partitionConfig, secondLevelPartitions, topLevelPartitions; if ((err != null) && err.code === 404) { _this._debug((getDocLink(_this.firstTopLevelID, _this.firstSecondLevelID, 'storage-engine-config')) + " not found. Creating a default config", header); secondLevelPartitions = {}; secondLevelPartitions[_this.firstSecondLevelID] = { id: _this.firstSecondLevelID }; topLevelPartitions = {}; topLevelPartitions[_this.firstTopLevelID] = { id: _this.firstTopLevelID, secondLevelPartitions: secondLevelPartitions }; partitionConfig = { topLevelPartitions: topLevelPartitions, topLevelLookupMap: { 'default': _this.firstTopLevelID } }; _this.storageEngineConfig = { id: 'storage-engine-config', mode: 'RUNNING', lastValidFrom: _this.LOWEST_DATE_STRING, partitionConfig: partitionConfig }; _this._debug('Setting @storageEngineConfig', _this.storageEngineConfig); return _this._writeConfig(function() { _this._readConfigContinuously(); return callback(); }); } else if (err != null) { callback(err); } else { _this.storageEngineConfig = response; _this._debug('Successfully retrieved @storageEngineConfig', _this.storageEngineConfig, header); _this.lastTimeConfigWasRead = new Date(); _this._readConfigContinuously(); if (callback != null) { return callback(null, _this.storageEngineConfig, header); } } }; })(this)); }; StorageEngine.prototype._writeConfig = function(retriesLeft, callback) { if (callback == null) { callback = retriesLeft; retriesLeft = null; } if (retriesLeft == null) { retriesLeft = 3; } return this.client.upsertDocument(getLink(this.firstTopLevelID, this.firstSecondLevelID), this.storageEngineConfig, (function(_this) { return function(err, response, header) { if (err != null) { callback(err); } else { _this._debug('Success: wrote @storageEngineConfig to database', response, header); if (callback != null) { return callback(); } } }; })(this)); }; StorageEngine.prototype._resolveToListOfPartitions = function(topLevelPartitionKey, secondLevelPartitionKey, originalPartitionConfig) { var collectionID, databaseID, hashring, partitionConfig, partitions, ref1, topLevelLookupMap, topLevelPartition, topLevelPartitions; partitions = []; if (originalPartitionConfig != null) { partitionConfig = originalPartitionConfig; } else { partitionConfig = this.storageEngineConfig.partitionConfig; } topLevelLookupMap = partitionConfig.topLevelLookupMap; topLevelPartitions = partitionConfig.topLevelPartitions; if (topLevelPartitionKey != null) { databaseID = topLevelLookupMap[topLevelPartitionKey]; if (databaseID == null) { databaseID = topLevelLookupMap['default']; } topLevelPartition = topLevelPartitions[databaseID]; if (secondLevelPartitionKey != null) { if (originalPartitionConfig != null) { hashring = new HashRing(topLevelPartition.secondLevelPartitions); } else if (((ref1 = this.secondLevelHashring) != null ? ref1.get : void 0) == null) { this.secondLevelHashring = new HashRing(topLevelPartition.secondLevelPartitions); hashring = this.secondLevelHashring; } else { hashring = this.secondLevelHashring; } collectionID = hashring.get(secondLevelPartitionKey); partitions.push(getLink(databaseID, collectionID)); } else { for (collectionID in topLevelPartition.secondLevelPartitions) { partitions.push(getLink(databaseID, collectionID)); } } } else { for (databaseID in topLevelPartitions) { topLevelPartition = topLevelPartitions[databaseID]; for (collectionID in topLevelPartition.secondLevelPartitions) { partitions.push(getLink(databaseID, collectionID)); } } } return partitions; }; StorageEngine.prototype.stop = function(callback) { /* */ this.storageEngineConfig.mode = 'STOPPED'; return this._writeConfig(callback); }; StorageEngine.prototype.start = function(callback) {}; StorageEngine.prototype.addCollections = function(callback) {}; StorageEngine.prototype.updatePartitionConfig = function(msBetweenPages, callback) { if (msBetweenPages == null) { return msBetweenPages = 0; } /* Apparently continuation tokens stick around for days so we can take a long time to crawl through one collection. */ }; StorageEngine.prototype.restartBalancing = function(msBetweenPages, callback) { if (msBetweenPages == null) { return msBetweenPages = 0; } /* The pseudo-code below is shown as loops, but we want to pick up each and every page as if it's restarting, so it'll recursive calls to this handler for each page. If it crashes, then there will be a part of the last page that's only partially done. It'll redo the page and find that only the documents that hadn't already moved are all that's left. If it crashes between the write and the delete, then the same id will be in two collections. However, when it restarts, and we go to write the duplicate to the new location, we'll reuse the id so there is no chance of creating duplicates in the same collection. Worst case, we'll need to swallow the duplicate id error so the delete can proceed. However, any reads that occur before it's deleted, will return the same row twice. */ }; StorageEngine.prototype.upsert = function(sessionID, upserts, temporalPolicy, callback) { if (temporalPolicy == null) { temporalPolicy = this.temporalPolicy; } /* This is a true incremental update. It will start with the most recent version of this entity and apply the field changes specified in the upsert call. If you want to remove a field, you must specify that field as null. Note, the null values are not stored so keep that in mind when composing queries. In general, you'll want to use {<fieldName>: $exists} rather than {$not: {<fieldName>: $isNull}}. The default temporalPolicy can be overridden for each call to upsertEntity. If there is an entity type that you don't want history for but others where you do, then you can control that on calls to upsert. Be careful to be consistent within an entity type. More commonly the temporalPolicy is set for the entire system during instantiation. Depending upon the temporalPolicy, entities are annotated with the Temporalize _ValidFrom/_ValidTo Richard Snodgrass mono-temporal data model. Note, transaction support for when _TemporalPolicy is 'NONE' is implemented by maintaining the _ValidFrom and _ValidTo fields just as it would for when it's not 'NONE', however, a 30-90 second delay is set to come back and delete the old version. This means that there are 30-90 seconds where two version of the entity exist. So, queries against these sort of entities should be made with asOf = 'LATEST'. No effort is made to enforce this except when the global @temporalPolicy is set to 'NONE'. In that case, the default for asOf on queries becomes 'LATEST'. */ return this._getSession(sessionID, (function(_this) { return function(err, session) { var i, len, ref1, upsert; for (i = 0, len = upserts.length; i < len; i++) { upsert = upserts[i]; if (ref1 = upsert[_this.topLevelPartitionField], indexOf.call(session.user.tenantIDsICanWrite, ref1) < 0) { transactionHandler({ code: 401, body: "User with username: " + session.user.username + " does not have permission to write to tenant with id: " + upsert[_this.topLevelPartitionField] }); return; } } return _this._upsert(upserts, temporalPolicy, callback); }; })(this)); }; StorageEngine.prototype._upsert = function(upserts, temporalPolicy, callback) { if (temporalPolicy == null) { temporalPolicy = this.temporalPolicy; } if (!this.terminate) { if (callback == null) { callback = temporalPolicy; temporalPolicy = null; } if (temporalPolicy == null) { temporalPolicy = this.temporalPolicy; } if (!_.isArray(upserts)) { upserts = [upserts]; } return this._readConfig((function(_this) { return function() { var i, len, results, t, transaction, transactionHandler, transactionID, transactionTimeString, upsert; t = new Date().toISOString(); if (t > _this.storageEngineConfig.lastValidFrom) { transactionTimeString = t; } else { transactionTimeString = new Date(new Date(_this.storageEngineConfig.lastValidFrom).valueOf() + 1).toISOString(); } transactionID = getGUID(); transaction = { id: transactionID, callback: callback, requestCount: 0, responseCount: 0, transactionTimeString: transactionTimeString, se: _this }; transactionHandler = _this._getTransactionHandler(transaction); transaction.transactionHandler = transactionHandler; transaction.entityIDsForThisTransaction = []; results = []; for (i = 0, len = upserts.length; i < len; i++) { upsert = upserts[i]; results.push(_this._upsertOne(upsert, transaction)); } return results; }; })(this)); } }; StorageEngine.prototype._upsertOne = function(upsert, transaction) { var collectionLink, partitionList, queryObject, queryString, ref1, transactionHandler; transactionHandler = transaction.transactionHandler; if (upsert[this.topLevelPartitionField] == null) { transactionHandler({ code: 400, body: "Every row in upserts must have a " + this.topLevelPartitionField + " field" }); return; } if (upsert[this.secondLevelPartitionField] == null) { transactionHandler({ code: 400, body: "Every row in upserts must have a " + this.secondLevelPartitionField + " field" }); return; } if (ref1 = upsert[this.secondLevelPartitionField], indexOf.call(transaction.entityIDsForThisTransaction, ref1) >= 0) { transactionHandler({ code: 400, body: this.secondLevelPartitionField + " must be not be duplicated in upsert list" }); return; } else { transaction.entityIDsForThisTransaction.push(upsert[this.secondLevelPartitionField]); } upsert._CreationTransactionID = transaction.id; upsert._ValidFrom = transaction.transactionTimeString; upsert._ValidTo = this.HIGHEST_DATE_STRING; queryObject = { _ValidTo: this.HIGHEST_DATE_STRING }; queryObject[this.secondLevelPartitionField] = upsert[this.secondLevelPartitionField]; queryString = sqlFromMongo(queryObject, 'c', '*'); partitionList = this._resolveToListOfPartitions(upsert[this.topLevelPartitionField], upsert[this.secondLevelPartitionField]); if (partitionList.length !== 1) { transactionHandler({ code: 403, body: "ERROR: partitionList.length for upsert() call should be 1. It is: " + partitionList.length }); return; } collectionLink = partitionList[0]; this._debug("Looking for documents with existing _EntityID with: `" + queryString + "`"); return this.client.queryDocuments(collectionLink, queryString).toArray((function(_this) { return function(err, response, header) { var key, newVersion, oldVersion, upsertCopy, value; if (err != null) { transactionHandler({ code: err.code, body: "Got '" + err.body + "' calling @client.queryDocuments() from within upsert" }); } else { if (response.length > 1) { transactionHandler({ code: 403, body: "Got more than one document with _ValidTo = " + _this.HIGHEST_DATE_STRING + " for " + _this.secondLevelPartitionField + " = " + upsert[_this.secondLevelPartitionField] }); } else if (response.length === 1) { _this._debug("Found old version for " + _this.secondLevelPartitionField + ": " + upsert[_this.secondLevelPartitionField] + "."); oldVersion = response[0]; newVersion = JSON.parse(JSON.stringify(oldVersion)); delete newVersion.id; newVersion._PreviousValues = {}; for (key in upsert) { value = upsert[key]; if (upsert[key] != null) { newVersion[key] = value; } else { delete newVersion[key]; } if (indexOf.call(_this.SYSTEM_FIELDS, key) < 0) { if (JSON.stringify(oldVersion[key]) !== JSON.stringify(newVersion[key])) { if (oldVersion[key] != null) { newVersion._PreviousValues[key] = oldVersion[key]; } else { newVersion._PreviousValues[key] = null; } } } } return _this.client.createDocument(collectionLink, newVersion, function(err, response, header) { var documentLink, newDocument, requestOptions; if (err != null) { transactionHandler({ code: err.code, body: "Got '" + err.body + "' calling @client.createDocument() from within upsert" }); } else { newDocument = response; _this._debug("Done writing new version for " + _this.secondLevelPartitionField + ": " + upsert[_this.secondLevelPartitionField] + ". Starting to update old version."); oldVersion._ValidTo = transaction.transactionTimeString; oldVersion._UpdateTransactionID = transaction.id; requestOptions = { accessCondition: { type: 'IfMatch', condition: oldVersion._etag } }; documentLink = collectionLink + ("/docs/" + oldVersion.id); transaction.requestCount++; return _this.client.replaceDocument(documentLink, oldVersion, requestOptions, transactionHandler); } }); } else { _this._debug("No old version for " + _this.secondLevelPartitionField + ": " + upsert[_this.secondLevelPartitionField] + ". Just need to add."); upsertCopy = JSON.parse(JSON.stringify(upsert)); upsertCopy._PreviousValues = {}; for (key in upsertCopy) { value = upsertCopy[key]; if (indexOf.call(_this.SYSTEM_FIELDS, key) < 0) { if (value != null) { upsertCopy._PreviousValues[key] = null; } } } transaction.requestCount++; return _this.client.createDocument(collectionLink, upsertCopy, transactionHandler); } } }; })(this)); }; StorageEngine.prototype.upsertUser = function(sessionID, user, password, callback) { var passwordStrength, session; if (callback == null) { callback = password; } if (user.password != null) { password = user.password; passwordStrength = zxcvbn(password); if (passwordStrength.score < 2) { callback({ code: 400, body: "Password is too weak. " + passwordStrength.feedback.warning + "." }); return; } else { delete user.password; } } user._IsTemporalizeUser = true; session = this.sessionCacheByUsername[user.username]; if (session != null) { delete this.sessionCacheByUsername[user.username]; delete this.sessionCacheByID[session.id]; } if (password != null) { user.salt = crypto.randomBytes(128).toString('base64'); return crypto.pbkdf2(password, user.salt, 10000, 512, (function(_this) { return function(err, dk) { if (err != null) { callback({ code: 400, body: "Got error trying to hash the provided password" }); } else { user.hash = dk.toString('base64'); return _this.upsert(sessionID, user, 'VALID_TIME', callback); } }; })(this)); } else { return this.upsert(sessionID, user, 'VALID_TIME', callback); } }; StorageEngine.prototype.login = function(username, password, callback) { var config, query; if (username == null) { callback({ code: 401, body: "Must provide a username when logging in." }); } if (password == null) { callback({ code: 401, body: "Must provide a password when logging in." }); } query = { _IsTemporalizeUser: true, username: username }; config = { query: query, asOf: 'LATEST' }; return this._query(config, (function(_this) { return function(err, result, memo) { var user; if (err != null) { return callback(err); } else if (result.length > 1) { return callback({ code: 400, body: "Found more than one user with username: " + username }); } else if (result.length < 1) { return callback({ code: 401, body: "Couldn't find user with username: " + username }); } else { user = result[0]; return crypto.pbkdf2(password, user.salt, 10000, 512, function(err, dk) { var calculatedHash, session; if (err != null) { callback({ code: 400, body: "Got error trying to hash the provided password" }); } else { calculatedHash = dk.toString('base64'); if (user.hash === calculatedHash) { session = {}; session.id = getGUID(); session._IsSession = true; session._Created = new Date().toISOString(); session.user = user; _this.sessionCacheByID[session.id] = session; _this.sessionCacheByUsername[user.username] = session; return callback(null, session); } else { return callback({ code: 401, body: "Password does not match" }); } } }); } }; })(this)); }; StorageEngine.prototype.logout = function(sessionID, callback) { var session; session = this.sessionCacheByID[sessionID]; if (session != null) { delete this.sessionCacheByID[sessionID]; delete this.sessionCacheByUsername[session.user.username]; } return callback(null, true); }; StorageEngine.prototype._getSession = function(sessionID, callback) { var session, sessionTTL; session = this.sessionCacheByID[sessionID]; if (session != null) { sessionTTL = session.user.sessionTTL || this.sessionTTL; if (new Date() - new Date(session._Created) < sessionTTL) { callback(null, session); return session; } else { delete this.sessionCacheByID[sessionID]; delete this.sessionCacheByUsername[session.user.username]; callback({ code: 401, body: "Session expired" }); } } else { callback({ code: 401, body: "Session not found" }); } }; StorageEngine.prototype._getTransactionHandler = function(transaction) { var f; f = function(err, response, header) { var se; if (transaction.err == null) { if (err != null) { transaction.err = err; return transaction.callback(err); } else { transaction.responseCount++; if (transaction.response == null) { transaction.response = []; } transaction.response.push(response); if (transaction.requestCount > transaction.responseCount) { } else { se = transaction.se; se.storageEngineConfig.lastValidFrom = transaction.transactionTimeString; if (transaction.response.length === 1) { transaction.response = transaction.response[0]; } return se._writeConfig(function(err) { return transaction.callback(err, transaction.response); }); } } } }; return f; }; StorageEngine.prototype.query = function(sessionID, config, callback) { /* sessionID [topLevelPartitionKey] [secondLevelPartitionKey] You must provide either a topLevelPartitionKey and a secondLevelPartitionKey or a query or all three. When a secondLevelPartitionKey is provided, the query defaults to {@secondLevelPartitionField: secondLevelPartitionKey} which will return the entire history of this one entity. If a query and a secondLevelPartitionKey is provided, the query is modified with {$and: [{@secondLevelPartitionField: secondLevelPartitionKey}, <oldQuery>]}. [query] Required unless a secondLevelPartitionKey is provided. MongoDB-like format. [fields] If no fields are specified, all are included. [maxItemCount] default: -1. To limit a query to a certain number of documents, provide a limit value. The response will include a continuation token that you can pass in to get the next page. Alternatively, you can do paging with a query clause on _ValidFrom but be careful to remove the last few documents of each page with the same _ValidFrom and use the prior _ValidFrom in the query clause for your next page. We may support this mode of paging automatically in the future. [continuationTokens] (not implemented) This is an object. The key is the partitionLink. The value is the continuation token for that partition. [includeDeleted] (not implemented) default: false. Unless includeDeleted == true, {_Deleted: {$exists: false}} is added to the $and clause. [asOf] When a asOf parameter is provided, the query will be done for that moment in time. ISO-8601 time formats are supported even partial ones like '2015-01' which specifies midnight GMT on January 1, 2015. Specifying asOf = 'LATEST' or 'latest' or 'LAST' or 'last' will use the lastValidTo value. Unless a asOf is specified, the query is modified with {$and: [<oldQuery>, {_ValidFrom: {$lte: lastValidFrom}]} to prevent partially completed transactions from being returned. When the global @temporalPolicy is set to 'NONE', the default asOf is 'LATEST'. */ return this._getSession(sessionID, (function(_this) { return function(err, session) { if (err != null) { return callback(err); } else { return _this._query(config, function(err, result, memo) { var authorizedForAll, i, len, ref1, row, unauthorizedTenantIDs; authorizedForAll = true; unauthorizedTenantIDs = []; for (i = 0, len = result.length; i < len; i++) { row = result[i]; if (ref1 = row[_this.topLevelPartitionField], indexOf.call(session.user.tenantIDsICanRead, ref1) < 0) { unauthorizedTenantIDs.push(row[_this.topLevelPartitionField]); authorizedForAll = false; } } if (authorizedForAll) { return callback(err, result, memo); } else { return callback({ code: 401, body: "Unauthorized for TenantIDs indicated in unauthorizedTenantIDs", unauthorizedTenantIDs: unauthorizedTenantIDs }); } }); } }; })(this)); }; StorageEngine.prototype._query = function(config, callback) { var modifiedQuery, msg, partitionList, queryOptions, ref1, sqlQuery; if (this.storageEngineConfig.mode === 'STOPPED') { msg = "Storage engine is currently stopped"; callback(msg); return msg; } if ((config.query != null) && !_.isPlainObject(config.query)) { callback(new Error("config.query must be a plain object when calling query().")); return; } if (!((config.query != null) || ((config.secondLevelPartitionKey != null) && (config.topLevelPartitionKey != null)))) { callback(new Error("Must provide config.query or (config.topLevelPartitionKey and config.secondLevelPartitionKey) when calling query().")); return; } if (config.maxItemCount == null) { config.maxItemCount = -1; } if (config.fields == null) { config.fields = '*'; } if (config.query != null) { modifiedQuery = JSON.parse(JSON.stringify(config.query)); } else { modifiedQuery = {}; } if (config.secondLevelPartitionKey != null) { modifiedQuery[this.secondLevelPartitionField] = config.secondLevelPartitionKey; } if (config.asOf != null) { if ((ref1 = config.asOf) === 'LATEST' || ref1 === 'LAST' || ref1 === 'latest' || ref1 === 'last') { config.asOf = this.storageEngineConfig.lastValidFrom; } modifiedQuery._ValidTo = { $gt: config.asOf }; modifiedQuery._ValidFrom = { $lte: config.asOf }; } else { modifiedQuery._ValidFrom = { $lte: this.storageEngineConfig.lastValidFrom }; } sqlQuery = sqlFromMongo(modifiedQuery, 'c', config.fields); queryOptions = { maxItemCount: config.maxItemCount }; partitionList = this._resolveToListOfPartitions(config.topLevelPartitionKey, config.secondLevelPartitionKey); this.client.queryDocumentsArrayMulti(partitionList, sqlQuery, queryOptions, callback); }; StorageEngine.prototype.undelete = function() {}; return StorageEngine; })(); }).call(this);