From fc9b4c4406d8fd0b05b281a7c16e0787e08f39af Mon Sep 17 00:00:00 2001 From: Alexander Mays Date: Sun, 7 Feb 2016 13:11:38 -0500 Subject: [PATCH 01/14] Added the memory cache and tests. Signed-off-by: Alexander Mays --- classes/BaseProvider.js | 21 + classes/CacheProvider.js | 25 ++ classes/MemoryCache.js | 162 ++++++++ interfaces/ServiceProvider.js | 12 + package.json | 1 + spec/classes/MemoryCache.spec.js | 642 +++++++++++++++++++++++++++++++ spec/support/jasmine.json | 2 +- src/index.js | 6 + 8 files changed, 870 insertions(+), 1 deletion(-) create mode 100644 classes/BaseProvider.js create mode 100644 classes/CacheProvider.js create mode 100644 classes/MemoryCache.js create mode 100644 interfaces/ServiceProvider.js create mode 100644 spec/classes/MemoryCache.spec.js diff --git a/classes/BaseProvider.js b/classes/BaseProvider.js new file mode 100644 index 0000000000..856646af06 --- /dev/null +++ b/classes/BaseProvider.js @@ -0,0 +1,21 @@ +var ServiceProviderInterface = require('../interfaces/ServiceProvider'); +var util = require('util'); + +function BaseProvider(adapter) { + this.adapter = adapter; +}; + +util.inherits(BaseProvider, ServiceProviderInterface); + +function getAdapter() { + return this.adapter; +} + +function setAdapter(adapter) { + this.adapter = adapter; +} + +BaseProvider.prototype.getAdapter = getAdapter; +BaseProvider.prototype.setAdapter = setAdapter; + +module.exports = BaseProvider; \ No newline at end of file diff --git a/classes/CacheProvider.js b/classes/CacheProvider.js new file mode 100644 index 0000000000..0866586412 --- /dev/null +++ b/classes/CacheProvider.js @@ -0,0 +1,25 @@ +var BaseProvider = require('./BaseProvider'); +var util = require('util'); + +// Singleton for the entire server. +// TODO: Refactor away from singleton paradigm +var instance = null; + +function CacheProvider(adapter) { + if (instance) { + return instance; + } + + instance = this; + + // Instantiate the adapter if the class got passed instead of an instance + if (typeof adapter === 'function') { + this.adapter = new adapter(); + } else { + this.adapter = adapter; + } +}; + +util.inherits(CacheProvider, BaseProvider); + +module.exports = CacheProvider; \ No newline at end of file diff --git a/classes/MemoryCache.js b/classes/MemoryCache.js new file mode 100644 index 0000000000..801353c448 --- /dev/null +++ b/classes/MemoryCache.js @@ -0,0 +1,162 @@ +'use strict'; +// Modified from https://github.com/ptarjan/node-cache/blob/master/index.js +function MemoryCache() { + this.cache = new Map(); + this.debug = false; + this.hitCount = 0; + this.missCount = 0; +}; + +function put (key, value, time, timeoutCallback) { + if (this.debug) { + console.log('caching: %s = %j (@%s)', key, value, time); + } + + if (typeof time !== 'undefined' && (typeof time !== 'number' || isNaN(time) || time <= 0)) { + throw new Error('Cache timeout must be a positive number'); + } else if (typeof timeoutCallback !== 'undefined' && typeof timeoutCallback !== 'function') { + throw new Error('Cache timeout callback must be a function'); + } + + var oldRecord = this.cache.get(key); + if (oldRecord) { + clearTimeout(oldRecord.timeout); + } + + var record = { + value: value, + expire: (time + Date.now()) + }; + + if (!isNaN(record.expire)) { + record.timeout = setTimeout(() => { + this.del(key); + if (timeoutCallback) { + timeoutCallback(key); + } + }, time); + } + + this.cache.set(key, record); + + return value; +}; + +function del (key) { + if (this.debug) { + console.log('Deleting key ', key); + } + var oldRecord = this.cache.get(key); + if (oldRecord) { + if (oldRecord.timeout) { + clearTimeout(oldRecord.timeout); + } + + this.cache.delete(key); + return true; + } + + return false; +}; + +function clear () { + for (var entry of this.cache) { + clearTimeout(entry[1].timeout); + } + this.cache = new Map(); + this.hitCount = 0; + this.missCount = 0; +}; + +function killTimer(key) { + var obj = this.cache.get(key); + if (obj && obj.timeout) { + clearTimeout(obj.timeout); + } +}; + +function get (key) { + var data = this.cache.get(key); + if (typeof data != "undefined") { + if (isNaN(data.expire) || data.expire >= Date.now()) { + this.hitCount++; + return data.value; + } else { + // free some space + this.missCount++; + this.del(key) + } + } else { + this.missCount++; + } + return null; +}; + +function size () { + return this.cache.size; +}; + +function setDebug (bool) { + this.debug = bool; +}; + +function hits () { + return this.hitCount; +}; + +function misses () { + return this.missCount; +}; + +function keys () { + return Array.from(this.cache.keys()); +}; + +function toArray() { + return Array.from(this.cache.values()); +} + +function map(functor, context) { + context = context || this; + var result = new Map(); + + for (var entry of this.cache.entries()) { + var key = entry[0]; + var value = entry[1]; + result.set(key, functor.call(context, value, key)); + } + + return result; +} + +function filter(predicate, context) { + context = context || this; + var result = new Map(); + + for (var entry of this.cache.entries()) { + var key = entry[0]; + var value = entry[1]; + + if (predicate.call(context, value, key)) { + result.set(key, value); + } + } + + return result; +} + +MemoryCache.prototype.put = put; +MemoryCache.prototype.get = get; +MemoryCache.prototype.del = del; +MemoryCache.prototype.clear = clear; +MemoryCache.prototype.killTimer = killTimer; +MemoryCache.prototype.size = size; +MemoryCache.prototype.hits = hits; +MemoryCache.prototype.misses = misses; +MemoryCache.prototype.keys = keys; +MemoryCache.prototype.setDebug = setDebug; +MemoryCache.prototype.toArray = toArray; +MemoryCache.prototype.map = map; +MemoryCache.prototype.filter = filter; + +module.exports = MemoryCache; \ No newline at end of file diff --git a/interfaces/ServiceProvider.js b/interfaces/ServiceProvider.js new file mode 100644 index 0000000000..c73c5f450c --- /dev/null +++ b/interfaces/ServiceProvider.js @@ -0,0 +1,12 @@ +function ServiceProviderInterface() { +}; + +ServiceProviderInterface.prototype.getAdapter = function() { + throw new Error('A service provider must implement getAdapter!'); +} + +ServiceProviderInterface.prototype.setAdapter = function() { + throw new Error('A service provider must implement setAdapter!'); +} + +module.exports = ServiceProviderInterface; \ No newline at end of file diff --git a/package.json b/package.json index 689110c01b..cd64187883 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "codecov": "^1.0.1", "deep-diff": "^0.3.3", "jasmine": "^2.3.2", + "lodash": "^4.2.1", "mongodb-runner": "^3.1.15" }, "scripts": { diff --git a/spec/classes/MemoryCache.spec.js b/spec/classes/MemoryCache.spec.js new file mode 100644 index 0000000000..d6eceb7c88 --- /dev/null +++ b/spec/classes/MemoryCache.spec.js @@ -0,0 +1,642 @@ +/* global describe, it, before, beforeEach, afterEach */ +'use strict'; + +var cache = new (require('../../classes/MemoryCache')); +var _ = require('lodash'); + +describe('MemoryCache', function() { + beforeEach(function() { + jasmine.clock().install(); + jasmine.clock().mockDate(); + jasmine.addMatchers({ + toDeepEqual: function(util, customEqualityTesters) { + return { + compare: function(actual, expected) { + var result = {}; + result.pass = _.isEqual(actual, expected); + return result; + } + } + } + }); + + cache.clear(); + }); + + afterEach(function() { + jasmine.clock().uninstall(); + }); + + describe('put()', function() { + beforeEach(function() { + cache.setDebug(false); + }); + + it('should allow adding a new item to the cache', function() { + expect(function() { + cache.put('key', 'value'); + }).not.toThrow(); + }); + + it('should allow adding a new item to the cache with a timeout', function() { + expect(function() { + cache.put('key', 'value', 100); + }).not.toThrow(); + }); + + it('should allow adding a new item to the cache with a timeout callback', function() { + expect(function() { + cache.put('key', 'value', 100, function() {}); + }).not.toThrow(); + }); + + it('should throw an error given a non-numeric timeout', function() { + expect(function() { + cache.put('key', 'value', 'foo'); + }).toThrow(); + }); + + it('should throw an error given a timeout of NaN', function() { + expect(function() { + cache.put('key', 'value', NaN); + }).toThrow(); + }); + + it('should throw an error given a timeout of 0', function() { + expect(function() { + cache.put('key', 'value', 0); + }).toThrow(); + }); + + it('should throw an error given a negative timeout', function() { + expect(function() { + cache.put('key', 'value', -100); + }).toThrow(); + }); + + it('should throw an error given a non-function timeout callback', function() { + expect(function() { + cache.put('key', 'value', 100, 'foo'); + }).toThrow(); + }); + + it('should cause the timeout callback to fire once the cache item expires', function() { + var callback = jasmine.createSpy('callback'); + cache.put('key', 'value', 1000, callback); + jasmine.clock().tick(999); + expect(callback).not.toHaveBeenCalled(); + jasmine.clock().tick(1); + expect(callback).toHaveBeenCalledWith('key'); + }); + + it('should override the timeout callback on a new put() with a different timeout callback', function() { + var spy1 = jasmine.createSpy(); + var spy2 = jasmine.createSpy(); + cache.put('key', 'value', 1000, spy1); + jasmine.clock().tick(999); + cache.put('key', 'value', 1000, spy2) + jasmine.clock().tick(1001); + expect(spy1).not.toHaveBeenCalled(); + expect(spy2).toHaveBeenCalledWith('key'); + }); + + it('should cancel the timeout callback on a new put() without a timeout callback', function() { + var spy = jasmine.createSpy(); + cache.put('key', 'value', 1000, spy); + jasmine.clock().tick(999); + cache.put('key', 'value') + jasmine.clock().tick(1); + expect(spy).not.toHaveBeenCalled(); + }); + + it('should return the cached value', function() { + expect(cache.put('key', 'value')).toEqual('value'); + }); + }); + + describe('del()', function() { + beforeEach(function() { + cache.setDebug(false); + }); + + it('should return false given a key for an empty cache', function() { + expect(cache.del('miss')).toBe(false); + }); + + it('should return false given a key not in a non-empty cache', function() { + cache.put('key', 'value'); + expect(cache.del('miss')).toBe(false); + }); + + it('should return true given a key in the cache', function() { + cache.put('key', 'value'); + expect(cache.del('key')).toBe(true); + }); + + it('should remove the provided key from the cache', function() { + cache.put('key', 'value'); + expect(cache.get('key')).toEqual('value'); + expect(cache.del('key')).toBe(true); + expect(cache.get('key')).toBe(null); + }); + + it('should decrement the cache size by 1', function() { + cache.put('key', 'value'); + expect(cache.size()).toEqual(1); + expect(cache.del('key')).toBe(true); + expect(cache.size()).toEqual(0); + }); + + it('should not remove other keys in the cache', function() { + cache.put('key1', 'value1'); + cache.put('key2', 'value2'); + cache.put('key3', 'value3'); + expect(cache.get('key1')).toEqual('value1'); + expect(cache.get('key2')).toEqual('value2'); + expect(cache.get('key3')).toEqual('value3'); + cache.del('key1'); + expect(cache.get('key1')).toBe(null); + expect(cache.get('key2')).toEqual('value2'); + expect(cache.get('key3')).toEqual('value3'); + }); + + it('should only delete a key from the cache once even if called multiple times in a row', function() { + cache.put('key1', 'value1'); + cache.put('key2', 'value2'); + cache.put('key3', 'value3'); + expect(cache.size()).toEqual(3); + cache.del('key1'); + cache.del('key1'); + cache.del('key1'); + expect(cache.size()).toEqual(2); + }); + + it('should handle deleting keys which were previously deleted and then re-added to the cache', function() { + cache.put('key', 'value'); + expect(cache.get('key')).toEqual('value'); + cache.del('key'); + expect(cache.get('key')).toBe(null); + cache.put('key', 'value'); + expect(cache.get('key')).toEqual('value'); + cache.del('key'); + expect(cache.get('key')).toBe(null); + }); + + it('should cancel the timeout callback for the deleted key', function() { + var spy = jasmine.createSpy(); + cache.put('key', 'value', 1000, spy); + cache.del('key'); + jasmine.clock().tick(1000); + expect(spy).not.toHaveBeenCalled(); + }); + }); + + describe('clear()', function() { + beforeEach(function() { + cache.setDebug(false); + }); + + it('should have no effect given an empty cache', function() { + expect(cache.size()).toEqual(0); + cache.clear(); + expect(cache.size()).toEqual(0); + }); + + it('should remove all existing keys in the cache', function() { + cache.put('key1', 'value1'); + cache.put('key2', 'value2'); + cache.put('key3', 'value3'); + expect(cache.size()).toEqual(3); + cache.clear(); + expect(cache.size()).toEqual(0); + }); + + it('should remove the keys in the cache', function() { + cache.put('key1', 'value1'); + cache.put('key2', 'value2'); + cache.put('key3', 'value3'); + expect(cache.get('key1')).toEqual('value1'); + expect(cache.get('key2')).toEqual('value2'); + expect(cache.get('key3')).toEqual('value3'); + cache.clear(); + expect(cache.get('key1')).toBe(null); + expect(cache.get('key2')).toBe(null); + expect(cache.get('key3')).toBe(null); + }); + + it('should reset the cache size to 0', function() { + cache.put('key1', 'value1'); + cache.put('key2', 'value2'); + cache.put('key3', 'value3'); + expect(cache.size()).toEqual(3); + cache.clear(); + expect(cache.size()).toEqual(0); + }); + + it('should reset the debug cache hits', function() { + cache.setDebug(false); + cache.put('key', 'value'); + cache.get('key'); + expect(cache.hits()).toEqual(1); + cache.clear(); + expect(cache.hits()).toEqual(0); + }); + + it('should reset the debug cache misses', function() { + cache.setDebug(false); + cache.put('key', 'value'); + cache.get('miss1'); + expect(cache.misses()).toEqual(1); + cache.clear(); + expect(cache.misses()).toEqual(0); + }); + + it('should cancel the timeout callbacks for all existing keys', function() { + var spy1 = jasmine.createSpy(); + var spy2 = jasmine.createSpy(); + var spy3 = jasmine.createSpy(); + cache.put('key1', 'value1', 1000, spy1); + cache.put('key2', 'value2', 1000, spy2); + cache.put('key3', 'value3', 1000, spy3); + cache.clear(); + jasmine.clock().tick(1000); + expect(spy1).not.toHaveBeenCalled(); + expect(spy2).not.toHaveBeenCalled(); + expect(spy3).not.toHaveBeenCalled(); + }); + }); + + describe('get()', function() { + beforeEach(function() { + cache.setDebug(false); + }); + + it('should return null given a key for an empty cache', function() { + expect(cache.get('miss')).toBe(null); + }); + + it('should return null given a key not in a non-empty cache', function() { + cache.put('key', 'value'); + expect(cache.get('miss')).toBe(null); + }); + + it('should return the corresponding value of a key in the cache', function() { + cache.put('key', 'value'); + expect(cache.get('key')).toEqual('value'); + }); + + it('should return the latest corresponding value of a key in the cache', function() { + cache.put('key', 'value1'); + cache.put('key', 'value2'); + cache.put('key', 'value3'); + expect(cache.get('key')).toEqual('value3'); + }); + + it('should handle various types of cache keys', function() { + var keys = [null, undefined, NaN, true, false, 0, 1, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY, '', 'a', [], {}, [1, 'a', false], {a:1,b:'a',c:false}, function() {}]; + keys.forEach(function(key, index) { + var value = 'value' + index; + cache.put(key, value); + expect(cache.get(key)).toDeepEqual(value); + }); + }); + + it('should handle various types of cache values', function() { + var values = [null, undefined, NaN, true, false, 0, 1, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY, '', 'a', [], {}, [1, 'a', false], {a:1,b:'a',c:false}, function() {}]; + values.forEach(function(value, index) { + var key = 'key' + index; + cache.put(key, value); + expect(cache.get(key)).toDeepEqual(value); + }); + }); + + it('should not set a timeout given no expiration time', function() { + cache.put('key', 'value'); + jasmine.clock().tick(1000); + expect(cache.get('key')).toEqual('value'); + }); + + it('should return the corresponding value of a non-expired key in the cache', function() { + cache.put('key', 'value', 1000); + jasmine.clock().tick(999); + expect(cache.get('key')).toEqual('value'); + }); + + it('should return null given an expired key', function() { + cache.put('key', 'value', 1000); + jasmine.clock().tick(1000); + expect(cache.get('key')).toBe(null); + }); + + it('should delete an object which has expired and is still in the cache', function() { + cache.setDebug(false); + cache.put('key', 'value', 10000); + cache.killTimer('key'); + jasmine.clock().tick(10001); + expect(cache.keys()).toDeepEqual(['key']); + cache.get('key'); + expect(cache.keys()).toDeepEqual([]); + }); + + it('should return null given a key which is a property on the Object prototype', function() { + expect(cache.get('toString')).toBe(null); + }); + + it('should allow reading the value for a key which is a property on the Object prototype', function() { + cache.put('toString', 'value'); + expect(cache.get('toString')).toEqual('value'); + }); + }); + + describe("killTimer()", function() { + it("should prevent a timer from being executed", function() { + // Sanity check + cache.put('key', 'value', 10000); + expect(cache.get('key')).toEqual('value'); + jasmine.clock().tick(10000); + expect(cache.get('key')).not.toEqual('value'); + + cache.put('key', 'value', 10000); + cache.killTimer('key'); + expect(cache.get('key')).toEqual('value'); + jasmine.clock().tick(10000); + expect(cache.get('key')).toEqual('value'); + }); + }); + + describe('size()', function() { + beforeEach(function() { + cache.setDebug(false); + }); + + it('should return 0 given a fresh cache', function() { + expect(cache.size()).toEqual(0); + }); + + it('should return 1 after adding a single item to the cache', function() { + cache.put('key', 'value'); + expect(cache.size()).toEqual(1); + }); + + it('should return 3 after adding three items to the cache', function() { + cache.put('key1', 'value1'); + cache.put('key2', 'value2'); + cache.put('key3', 'value3'); + expect(cache.size()).toEqual(3); + }); + + it('should not multi-count duplicate items added to the cache', function() { + cache.put('key', 'value1'); + expect(cache.size()).toEqual(1); + cache.put('key', 'value2'); + expect(cache.size()).toEqual(1); + }); + + it('should update when a key in the cache expires', function() { + cache.put('key', 'value', 1000); + expect(cache.size()).toEqual(1); + jasmine.clock().tick(999); + expect(cache.size()).toEqual(1); + jasmine.clock().tick(1); + expect(cache.size()).toEqual(0); + }); + }); + + describe('debug()', function() { + it('should change the value of the debug property', function() { + expect(cache.debug).toEqual(false); + cache.setDebug(true); + expect(cache.debug).toEqual(true); + }); + }); + + describe('hits()', function() { + beforeEach(function() { + cache.setDebug(false); + }); + + it('should return 0 given an empty cache', function() { + expect(cache.hits()).toEqual(0); + }); + + it('should return 0 given a non-empty cache which has not been accessed', function() { + cache.put('key', 'value'); + expect(cache.hits()).toEqual(0); + }); + + it('should return 0 given a non-empty cache which has had only misses', function() { + cache.put('key', 'value'); + cache.get('miss1'); + cache.get('miss2'); + cache.get('miss3'); + expect(cache.hits()).toEqual(0); + }); + + it('should return 1 given a non-empty cache which has had a single hit', function() { + cache.put('key', 'value'); + cache.get('key'); + expect(cache.hits()).toEqual(1); + }); + + it('should return 3 given a non-empty cache which has had three hits on the same key', function() { + cache.put('key', 'value'); + cache.get('key'); + cache.get('key'); + cache.get('key'); + expect(cache.hits()).toEqual(3); + }); + + it('should return 3 given a non-empty cache which has had three hits across many keys', function() { + cache.put('key1', 'value1'); + cache.put('key2', 'value2'); + cache.put('key3', 'value3'); + cache.get('key1'); + cache.get('key2'); + cache.get('key3'); + expect(cache.hits()).toEqual(3); + }); + + it('should return the correct value after a sequence of hits and misses', function() { + cache.put('key1', 'value1'); + cache.put('key2', 'value2'); + cache.put('key3', 'value3'); + cache.get('key1'); + cache.get('miss'); + cache.get('key3'); + expect(cache.hits()).toEqual(2); + }); + + it('should not count hits for expired keys', function() { + cache.put('key', 'value', 1000); + cache.get('key'); + expect(cache.hits()).toEqual(1); + jasmine.clock().tick(999); + cache.get('key'); + expect(cache.hits()).toEqual(2); + jasmine.clock().tick(1); + cache.get('key'); + expect(cache.hits()).toEqual(2); + }); + }); + + describe('misses()', function() { + beforeEach(function() { + cache.setDebug(false); + }); + + it('should return 0 given an empty cache', function() { + expect(cache.misses()).toEqual(0); + }); + + it('should return 0 given a non-empty cache which has not been accessed', function() { + cache.put('key', 'value'); + expect(cache.misses()).toEqual(0); + }); + + it('should return 0 given a non-empty cache which has had only hits', function() { + cache.put('key', 'value'); + cache.get('key'); + cache.get('key'); + cache.get('key'); + expect(cache.misses()).toEqual(0); + }); + + it('should return 1 given a non-empty cache which has had a single miss', function() { + cache.put('key', 'value'); + cache.get('miss'); + expect(cache.misses()).toEqual(1); + }); + + it('should return 3 given a non-empty cache which has had three misses', function() { + cache.put('key', 'value'); + cache.get('miss1'); + cache.get('miss2'); + cache.get('miss3'); + expect(cache.misses()).toEqual(3); + }); + + it('should return the correct value after a sequence of hits and misses', function() { + cache.put('key1', 'value1'); + cache.put('key2', 'value2'); + cache.put('key3', 'value3'); + cache.get('key1'); + cache.get('miss'); + cache.get('key3'); + expect(cache.misses()).toEqual(1); + }); + + it('should count misses for expired keys', function() { + cache.put('key', 'value', 1000); + cache.get('key'); + expect(cache.misses()).toEqual(0); + jasmine.clock().tick(999); + cache.get('key'); + expect(cache.misses()).toEqual(0); + jasmine.clock().tick(1); + cache.get('key'); + expect(cache.misses()).toEqual(1); + }); + }); + + describe('keys()', function() { + beforeEach(function() { + cache.setDebug(false); + }); + + it('should return an empty array given an empty cache', function() { + expect(cache.keys()).toDeepEqual([]); + }); + + it('should return a single key after adding a single item to the cache', function() { + cache.put('key', 'value'); + expect(cache.keys()).toDeepEqual(['key']); + }); + + it('should return 3 keys after adding three items to the cache', function() { + cache.put('key1', 'value1'); + cache.put('key2', 'value2'); + cache.put('key3', 'value3'); + expect(cache.keys()).toDeepEqual(['key1', 'key2', 'key3']); + }); + + it('should not multi-count duplicate items added to the cache', function() { + cache.put('key', 'value1'); + expect(cache.keys()).toDeepEqual(['key']); + cache.put('key', 'value2'); + expect(cache.keys()).toDeepEqual(['key']); + }); + + it('should update when a key in the cache expires', function() { + cache.put('key', 'value', 1000); + expect(cache.keys()).toDeepEqual(['key']); + jasmine.clock().tick(999); + expect(cache.keys()).toDeepEqual(['key']); + jasmine.clock().tick(1); + expect(cache.keys()).toDeepEqual([]); + }); + }); + + describe('toArray()', function() { + beforeEach(function() { + cache.setDebug(false); + }); + + it("should return an array of values", function() { + cache.put('key1', 'value1'); + cache.put('key2', 'value2'); + expect(cache.toArray()).toDeepEqual([ + { expire: NaN, value: 'value1' }, + { expire: NaN, value: 'value2' } + ]); + }); + }); + + describe('filter()', function() { + beforeEach(function() { + cache.setDebug(false); + }); + + it("should filter based on a predicate", function() { + cache.put('key1', 'value1'); + cache.put('key2', 'value2'); + var filtered = cache.filter(function(item) { + return item.value == 'value1'; + }); + expect(filtered.get('key1')).toDeepEqual({ expire: NaN, value: 'value1' }); + expect(filtered.get('key2')).toEqual(undefined); + }); + + it("should filter all keys without expirations", function() { + cache.put('key1', 'value1'); + cache.put('key2', 'value2'); + cache.put('key3', 'value3', 10000); + cache.put('key4', 'value4', 20000); + var filtered = cache.filter(function(item) { + return isNaN(item.expire); + }) + expect(filtered.get('key1')).toDeepEqual({ expire: NaN, value: 'value1' }); + expect(filtered.get('key2')).toDeepEqual({ expire: NaN, value: 'value2' }); + expect(filtered.get('key3')).toEqual(undefined); + expect(filtered.get('key4')).toEqual(undefined); + }); + }); + + + + describe("map()", function() { + it("should map the values of the cache", function() { + cache.put('key1', 1); + cache.put('key2', 2); + cache.put('key3', 3); + cache.put('key4', 4); + var mapped = cache.map(function(value, key) { + value.value = value.value + 1; + return value; + }); + expect(mapped.get('key1')).toDeepEqual({ expire: NaN, value: 2 }); + expect(mapped.get('key2')).toDeepEqual({ expire: NaN, value: 3 }); + expect(mapped.get('key3')).toDeepEqual({ expire: NaN, value: 4 }); + expect(mapped.get('key4')).toDeepEqual({ expire: NaN, value: 5 }); + }) + }) +}); diff --git a/spec/support/jasmine.json b/spec/support/jasmine.json index e0347ebfe7..c7cd85f83e 100644 --- a/spec/support/jasmine.json +++ b/spec/support/jasmine.json @@ -1,7 +1,7 @@ { "spec_dir": "spec", "spec_files": [ - "*spec.js" + "**/*spec.js" ], "helpers": [ "../node_modules/babel-core/register.js", diff --git a/src/index.js b/src/index.js index 48d3e8c9d7..0ef88a57f4 100644 --- a/src/index.js +++ b/src/index.js @@ -9,6 +9,7 @@ var batch = require('./batch'), middlewares = require('./middlewares'), multer = require('multer'), Parse = require('parse/node').Parse, + BaseProvider = require('./classes/BaseProvider'), PromiseRouter = require('./PromiseRouter'), httpRequest = require('./httpRequest'); @@ -40,11 +41,16 @@ addParseCloud(); // "dotNetKey": optional key from Parse dashboard // "restAPIKey": optional key from Parse dashboard // "javascriptKey": optional key from Parse dashboard + +var DefaultCacheAdapter = require('./classes/MemoryCache'); + function ParseServer(args) { if (!args.appId || !args.masterKey) { throw 'You must provide an appId and masterKey!'; } + this.cacheProvider = new BaseProvider(args.cacheAdapter || DefaultCacheAdapter); + if (args.databaseAdapter) { DatabaseAdapter.setAdapter(args.databaseAdapter); } From ce402156072065744a19bb14d9d3d58fcd066247 Mon Sep 17 00:00:00 2001 From: Alexander Mays Date: Sun, 7 Feb 2016 14:02:51 -0500 Subject: [PATCH 02/14] Implemented the new cache/provider to replace the old cache.js. All tests passing. Signed-off-by: Alexander Mays --- classes/CacheProvider.js | 5 +++++ spec/ParseInstallation.spec.js | 7 +++---- spec/RestCreate.spec.js | 7 +++---- spec/RestQuery.spec.js | 7 +++---- spec/helper.js | 3 +-- src/Auth.js | 9 +++++---- src/Config.js | 5 +++-- src/DatabaseAdapter.js | 5 +++-- src/RestWrite.js | 1 - src/cache.js | 37 ---------------------------------- src/index.js | 23 ++++++++++++++++----- src/middlewares.js | 19 +++++++++-------- src/rest.js | 6 +++--- src/testing-routes.js | 10 +++++---- 14 files changed, 62 insertions(+), 82 deletions(-) delete mode 100644 src/cache.js diff --git a/classes/CacheProvider.js b/classes/CacheProvider.js index 0866586412..74b66f227d 100644 --- a/classes/CacheProvider.js +++ b/classes/CacheProvider.js @@ -12,6 +12,11 @@ function CacheProvider(adapter) { instance = this; + // Support passing in adapter paths + if (typeof adapter === 'string') { + adapter = require(adapter); + } + // Instantiate the adapter if the class got passed instead of an instance if (typeof adapter === 'function') { this.adapter = new adapter(); diff --git a/spec/ParseInstallation.spec.js b/spec/ParseInstallation.spec.js index 91bb9a23b4..1c53c98d96 100644 --- a/spec/ParseInstallation.spec.js +++ b/spec/ParseInstallation.spec.js @@ -1,10 +1,9 @@ // These tests check the Installations functionality of the REST API. // Ported from installation_collection_test.go -var auth = require('../src/Auth'); -var cache = require('../src/cache'); -var Config = require('../src/Config'); -var DatabaseAdapter = require('../src/DatabaseAdapter'); +var auth = require('../Auth'); +var Config = require('../Config'); +var DatabaseAdapter = require('../DatabaseAdapter'); var Parse = require('parse/node').Parse; var rest = require('../src/rest'); diff --git a/spec/RestCreate.spec.js b/spec/RestCreate.spec.js index 244555075a..b2f5b89b22 100644 --- a/spec/RestCreate.spec.js +++ b/spec/RestCreate.spec.js @@ -1,8 +1,7 @@ // These tests check the "create" functionality of the REST API. -var auth = require('../src/Auth'); -var cache = require('../src/cache'); -var Config = require('../src/Config'); -var DatabaseAdapter = require('../src/DatabaseAdapter'); +var auth = require('../Auth'); +var Config = require('../Config'); +var DatabaseAdapter = require('../DatabaseAdapter'); var Parse = require('parse/node').Parse; var rest = require('../src/rest'); var request = require('request'); diff --git a/spec/RestQuery.spec.js b/spec/RestQuery.spec.js index b93a07d588..e2aca8d90a 100644 --- a/spec/RestQuery.spec.js +++ b/spec/RestQuery.spec.js @@ -1,8 +1,7 @@ // These tests check the "find" functionality of the REST API. -var auth = require('../src/Auth'); -var cache = require('../src/cache'); -var Config = require('../src/Config'); -var rest = require('../src/rest'); +var auth = require('../Auth'); +var Config = require('../Config'); +var rest = require('../rest'); var config = new Config('test'); var nobody = auth.nobody(config); diff --git a/spec/helper.js b/spec/helper.js index 3e6c6d9853..3f762b26a7 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -2,8 +2,7 @@ jasmine.DEFAULT_TIMEOUT_INTERVAL = 2000; -var cache = require('../src/cache'); -var DatabaseAdapter = require('../src/DatabaseAdapter'); +var DatabaseAdapter = require('../DatabaseAdapter'); var express = require('express'); var facebook = require('../src/facebook'); var ParseServer = require('../src/index').ParseServer; diff --git a/src/Auth.js b/src/Auth.js index ad9056549a..4b5b64ff96 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -2,8 +2,6 @@ var deepcopy = require('deepcopy'); var Parse = require('parse/node').Parse; var RestQuery = require('./RestQuery'); -var cache = require('./cache'); - // An Auth object tells you who is requesting something and whether // the master key was used. // userObject is a Parse.User and can be null if there's no user. @@ -43,7 +41,10 @@ function nobody(config) { // Returns a promise that resolves to an Auth object var getAuthForSessionToken = function(config, sessionToken) { - var cachedUser = cache.getUser(sessionToken); + var cacheProvider = new (require('./classes/CacheProvider')); + var cache = cacheProvider.getAdapter(); + + var cachedUser = cache.get(sessionToken); if (cachedUser) { return Promise.resolve(new Auth(config, false, cachedUser)); } @@ -66,7 +67,7 @@ var getAuthForSessionToken = function(config, sessionToken) { obj['className'] = '_User'; obj['sessionToken'] = sessionToken; var userObject = Parse.Object.fromJSON(obj); - cache.setUser(sessionToken, userObject); + cache.put(sessionToken, userObject); return new Auth(config, false, userObject); }); }; diff --git a/src/Config.js b/src/Config.js index df44f8b170..3ab9eec4f4 100644 --- a/src/Config.js +++ b/src/Config.js @@ -2,10 +2,11 @@ // configured. // mount is the URL for the root of the API; includes http, domain, etc. function Config(applicationId, mount) { - var cache = require('./cache'); + var cacheProvider = new (require('./classes/CacheProvider')); + var cache = cacheProvider.getAdapter(); var DatabaseAdapter = require('./DatabaseAdapter'); - var cacheInfo = cache.apps[applicationId]; + var cacheInfo = cache.get(applicationId); this.valid = !!cacheInfo; if (!this.valid) { return; diff --git a/src/DatabaseAdapter.js b/src/DatabaseAdapter.js index 4967d5665d..81891d968d 100644 --- a/src/DatabaseAdapter.js +++ b/src/DatabaseAdapter.js @@ -17,7 +17,6 @@ var ExportAdapter = require('./ExportAdapter'); var adapter = ExportAdapter; -var cache = require('./cache'); var dbConnections = {}; var databaseURI = 'mongodb://localhost:27017/parse'; var appDatabaseURIs = {}; @@ -35,13 +34,15 @@ function setAppDatabaseURI(appId, uri) { } function getDatabaseConnection(appId) { + var cacheProvider = new (require('./classes/CacheProvider')); + var cache = cacheProvider.getAdapter(); if (dbConnections[appId]) { return dbConnections[appId]; } var dbURI = (appDatabaseURIs[appId] ? appDatabaseURIs[appId] : databaseURI); dbConnections[appId] = new adapter(dbURI, { - collectionPrefix: cache.apps[appId]['collectionPrefix'] + collectionPrefix: cache.get(appId)['collectionPrefix'] }); dbConnections[appId].connect(); return dbConnections[appId]; diff --git a/src/RestWrite.js b/src/RestWrite.js index 446a2db9a2..9a33f99a9a 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -7,7 +7,6 @@ var deepcopy = require('deepcopy'); var rack = require('hat').rack(); var Auth = require('./Auth'); -var cache = require('./cache'); var Config = require('./Config'); var passwordCrypto = require('./password'); var facebook = require('./facebook'); diff --git a/src/cache.js b/src/cache.js deleted file mode 100644 index aba6ce16ff..0000000000 --- a/src/cache.js +++ /dev/null @@ -1,37 +0,0 @@ -var apps = {}; -var stats = {}; -var isLoaded = false; -var users = {}; - -function getApp(app, callback) { - if (apps[app]) return callback(true, apps[app]); - return callback(false); -} - -function updateStat(key, value) { - stats[key] = value; -} - -function getUser(sessionToken) { - if (users[sessionToken]) return users[sessionToken]; - return undefined; -} - -function setUser(sessionToken, userObject) { - users[sessionToken] = userObject; -} - -function clearUser(sessionToken) { - delete users[sessionToken]; -} - -module.exports = { - apps: apps, - stats: stats, - isLoaded: isLoaded, - getApp: getApp, - updateStat: updateStat, - clearUser: clearUser, - getUser: getUser, - setUser: setUser -}; diff --git a/src/index.js b/src/index.js index 0ef88a57f4..ec2dd1bb15 100644 --- a/src/index.js +++ b/src/index.js @@ -2,14 +2,13 @@ var batch = require('./batch'), bodyParser = require('body-parser'), - cache = require('./cache'), + CacheProvider = require('./classes/CacheProvider'), DatabaseAdapter = require('./DatabaseAdapter'), express = require('express'), S3Adapter = require('./S3Adapter'), middlewares = require('./middlewares'), multer = require('multer'), Parse = require('parse/node').Parse, - BaseProvider = require('./classes/BaseProvider'), PromiseRouter = require('./PromiseRouter'), httpRequest = require('./httpRequest'); @@ -49,7 +48,11 @@ function ParseServer(args) { throw 'You must provide an appId and masterKey!'; } - this.cacheProvider = new BaseProvider(args.cacheAdapter || DefaultCacheAdapter); + this.setupCache(args.cache); + + var cache = this.cacheProvider.getAdapter(); + + console.log(cache); if (args.databaseAdapter) { DatabaseAdapter.setAdapter(args.databaseAdapter); @@ -74,7 +77,7 @@ function ParseServer(args) { } - cache.apps[args.appId] = { + var appInfo = { masterKey: args.masterKey, collectionPrefix: args.collectionPrefix || '', clientKey: args.clientKey || '', @@ -87,9 +90,11 @@ function ParseServer(args) { // To maintain compatibility. TODO: Remove in v2.1 if (process.env.FACEBOOK_APP_ID) { - cache.apps[args.appId]['facebookAppIds'].push(process.env.FACEBOOK_APP_ID); + appInfo['facebookAppIds'].push(process.env.FACEBOOK_APP_ID); } + cache.put(args.appId, appInfo); + // Initialize the node client SDK automatically Parse.initialize(args.appId, args.javascriptKey || '', args.masterKey); if(args.serverURL) { @@ -176,6 +181,14 @@ function getClassName(parseClass) { return parseClass; } +// TODO: Add configurable TTLs for cache entries +function setupCache(config) { + config = config || {}; + this.cacheProvider = new CacheProvider(config.adapter || DefaultCacheAdapter); +} + +ParseServer.prototype.setupCache = setupCache; + module.exports = { ParseServer: ParseServer, S3Adapter: S3Adapter diff --git a/src/middlewares.js b/src/middlewares.js index bb2512391a..ba9c52cf28 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -1,7 +1,6 @@ var Parse = require('parse/node').Parse; var auth = require('./Auth'); -var cache = require('./cache'); var Config = require('./Config'); // Checks that the request is authorized for this app and checks user @@ -11,6 +10,8 @@ var Config = require('./Config'); // req.config - the Config for this app // req.auth - the Auth for this request function handleParseHeaders(req, res, next) { + var cacheProvider = new (require('./classes/CacheProvider')); + var cache = cacheProvider.getAdapter(); var mountPathLength = req.originalUrl.length - req.url.length; var mountPath = req.originalUrl.slice(0, mountPathLength); var mount = req.protocol + '://' + req.get('host') + mountPath; @@ -27,8 +28,9 @@ function handleParseHeaders(req, res, next) { }; var fileViaJSON = false; + var app = cache.get(info.appId); - if (!info.appId || !cache.apps[info.appId]) { + if (!info.appId || !app) { // See if we can find the app id on the body. if (req.body instanceof Buffer) { // The only chance to find the app id is if this is a file @@ -37,13 +39,10 @@ function handleParseHeaders(req, res, next) { fileViaJSON = true; } - if (req.body && req.body._ApplicationId - && cache.apps[req.body._ApplicationId] - && ( - !info.masterKey - || - cache.apps[req.body._ApplicationId]['masterKey'] === info.masterKey) - ) { + if (req.body && req.body._ApplicationId) + app = cache.get(req.body._ApplicationId) + + if (app && (!info.masterKey || app.masterKey === info.masterKey)) { info.appId = req.body._ApplicationId; info.javascriptKey = req.body._JavaScriptKey || ''; delete req.body._ApplicationId; @@ -77,7 +76,7 @@ function handleParseHeaders(req, res, next) { req.body = new Buffer(base64, 'base64'); } - info.app = cache.apps[info.appId]; + info.app = cache.get(info.appId); req.config = new Config(info.appId, mount); req.database = req.config.database; req.info = info; diff --git a/src/rest.js b/src/rest.js index 552fa6be8c..13f2ff2e78 100644 --- a/src/rest.js +++ b/src/rest.js @@ -8,8 +8,6 @@ // things. var Parse = require('parse/node').Parse; - -var cache = require('./cache'); var RestQuery = require('./RestQuery'); var RestWrite = require('./RestWrite'); var triggers = require('./triggers'); @@ -37,6 +35,8 @@ function del(config, auth, className, objectId) { enforceRoleSecurity('delete', className, auth); var inflatedObject; + var cacheProvider = new (require('./classes/CacheProvider')); + var cache = cacheProvider.getAdapter(); return Promise.resolve().then(() => { if (triggers.getTrigger(className, 'beforeDelete') || @@ -46,7 +46,7 @@ function del(config, auth, className, objectId) { .then((response) => { if (response && response.results && response.results.length) { response.results[0].className = className; - cache.clearUser(response.results[0].sessionToken); + cache.del(response.results[0].sessionToken); inflatedObject = Parse.Object.fromJSON(response.results[0]); return triggers.maybeRunTrigger('beforeDelete', auth, inflatedObject); diff --git a/src/testing-routes.js b/src/testing-routes.js index 85db148516..03d8bb32c6 100644 --- a/src/testing-routes.js +++ b/src/testing-routes.js @@ -1,7 +1,6 @@ // testing-routes.js var express = require('express'), - cache = require('./cache'), middlewares = require('./middlewares'), rack = require('hat').rack(); @@ -9,11 +8,14 @@ var router = express.Router(); // creates a unique app in the cache, with a collection prefix function createApp(req, res) { + var cacheProvider = new (require('./classes/CacheProvider')); + var cache = cacheProvider.getAdapter(); var appId = rack(); - cache.apps[appId] = { + cache.put(appId, { 'collectionPrefix': appId + '_', 'masterKey': 'master' - }; + }); + var keys = { 'application_id': appId, 'client_key': 'unused', @@ -42,7 +44,7 @@ function dropApp(req, res) { return res.status(401).send({"error": "unauthorized"}); } req.database.deleteEverything().then(() => { - delete cache.apps[req.config.applicationId]; + cache.del(req.config.applicationId); res.status(200).send({}); }); } From 94835a3106cc885cb15f4379334ad44c5bd634fa Mon Sep 17 00:00:00 2001 From: Alexander Mays Date: Sun, 7 Feb 2016 20:56:44 -0500 Subject: [PATCH 03/14] Added default TTL Signed-off-by: Alexander Mays --- classes/CacheProvider.js | 13 +- classes/MemoryCache.js | 24 +- spec/classes/MemoryCache.spec.js | 1022 +++++++++++++++--------------- src/index.js | 24 +- src/testing-routes.js | 2 +- 5 files changed, 551 insertions(+), 534 deletions(-) diff --git a/classes/CacheProvider.js b/classes/CacheProvider.js index 74b66f227d..5131ea0fce 100644 --- a/classes/CacheProvider.js +++ b/classes/CacheProvider.js @@ -11,18 +11,7 @@ function CacheProvider(adapter) { } instance = this; - - // Support passing in adapter paths - if (typeof adapter === 'string') { - adapter = require(adapter); - } - - // Instantiate the adapter if the class got passed instead of an instance - if (typeof adapter === 'function') { - this.adapter = new adapter(); - } else { - this.adapter = adapter; - } + this.adapter = adapter; }; util.inherits(CacheProvider, BaseProvider); diff --git a/classes/MemoryCache.js b/classes/MemoryCache.js index 801353c448..c43e31898c 100644 --- a/classes/MemoryCache.js +++ b/classes/MemoryCache.js @@ -1,23 +1,31 @@ 'use strict'; // Modified from https://github.com/ptarjan/node-cache/blob/master/index.js -function MemoryCache() { +function MemoryCache(options) { + options = options || {}; + this.cache = new Map(); this.debug = false; this.hitCount = 0; this.missCount = 0; + this.defaultTtl = options.defaultTtl || 10 * 60 * 1000; }; -function put (key, value, time, timeoutCallback) { +function put (key, value, ttl, timeoutCallback) { if (this.debug) { - console.log('caching: %s = %j (@%s)', key, value, time); + console.log('caching: %s = %j (@%s)', key, value, ttl); } - if (typeof time !== 'undefined' && (typeof time !== 'number' || isNaN(time) || time <= 0)) { + if (typeof ttl !== 'undefined' && (typeof ttl !== 'number' || isNaN(ttl) || ttl <= 0)) { throw new Error('Cache timeout must be a positive number'); } else if (typeof timeoutCallback !== 'undefined' && typeof timeoutCallback !== 'function') { throw new Error('Cache timeout callback must be a function'); } + // TTL can still be set to Infinity for never expiring records + if (ttl === undefined) { + ttl = this.defaultTtl; + } + var oldRecord = this.cache.get(key); if (oldRecord) { clearTimeout(oldRecord.timeout); @@ -25,16 +33,16 @@ function put (key, value, time, timeoutCallback) { var record = { value: value, - expire: (time + Date.now()) + expire: (ttl + Date.now()) }; - if (!isNaN(record.expire)) { + if (!isNaN(record.expire) && ttl !== Infinity) { record.timeout = setTimeout(() => { this.del(key); if (timeoutCallback) { timeoutCallback(key); } - }, time); + }, ttl); } this.cache.set(key, record); @@ -89,7 +97,7 @@ function get (key) { } else { this.missCount++; } - return null; + return undefined; }; function size () { diff --git a/spec/classes/MemoryCache.spec.js b/spec/classes/MemoryCache.spec.js index d6eceb7c88..87e9e70a03 100644 --- a/spec/classes/MemoryCache.spec.js +++ b/spec/classes/MemoryCache.spec.js @@ -5,348 +5,354 @@ var cache = new (require('../../classes/MemoryCache')); var _ = require('lodash'); describe('MemoryCache', function() { - beforeEach(function() { - jasmine.clock().install(); - jasmine.clock().mockDate(); - jasmine.addMatchers({ - toDeepEqual: function(util, customEqualityTesters) { - return { - compare: function(actual, expected) { - var result = {}; - result.pass = _.isEqual(actual, expected); - return result; + beforeEach(function() { + jasmine.clock().install(); + jasmine.clock().mockDate(); + jasmine.addMatchers({ + toDeepEqual: function(util, customEqualityTesters) { + return { + compare: function(actual, expected) { + var result = {}; + result.pass = _.isEqual(actual, expected); + return result; + } } } - } + }); + + cache.clear(); }); - cache.clear(); - }); + afterEach(function() { + jasmine.clock().uninstall(); + }); - afterEach(function() { - jasmine.clock().uninstall(); - }); + describe('put()', function() { + beforeEach(function() { + cache.setDebug(false); + }); - describe('put()', function() { - beforeEach(function() { - cache.setDebug(false); - }); + it('should allow adding a new item to the cache', function() { + expect(function() { + cache.put('key', 'value'); + }).not.toThrow(); + }); - it('should allow adding a new item to the cache', function() { - expect(function() { - cache.put('key', 'value'); - }).not.toThrow(); - }); + it('should allow adding a new item to the cache with a timeout', function() { + expect(function() { + cache.put('key', 'value', 100); + }).not.toThrow(); + }); - it('should allow adding a new item to the cache with a timeout', function() { - expect(function() { - cache.put('key', 'value', 100); - }).not.toThrow(); - }); + it('should allow adding a new item to the cache with a timeout callback', function() { + expect(function() { + cache.put('key', 'value', 100, function() {}); + }).not.toThrow(); + }); - it('should allow adding a new item to the cache with a timeout callback', function() { - expect(function() { - cache.put('key', 'value', 100, function() {}); - }).not.toThrow(); - }); + it('should throw an error given a non-numeric timeout', function() { + expect(function() { + cache.put('key', 'value', 'foo'); + }).toThrow(); + }); - it('should throw an error given a non-numeric timeout', function() { - expect(function() { - cache.put('key', 'value', 'foo'); - }).toThrow(); - }); + it('should throw an error given a timeout of NaN', function() { + expect(function() { + cache.put('key', 'value', NaN); + }).toThrow(); + }); - it('should throw an error given a timeout of NaN', function() { - expect(function() { - cache.put('key', 'value', NaN); - }).toThrow(); - }); + it('should throw an error given a timeout of 0', function() { + expect(function() { + cache.put('key', 'value', 0); + }).toThrow(); + }); - it('should throw an error given a timeout of 0', function() { - expect(function() { - cache.put('key', 'value', 0); - }).toThrow(); - }); + it('should throw an error given a negative timeout', function() { + expect(function() { + cache.put('key', 'value', -100); + }).toThrow(); + }); - it('should throw an error given a negative timeout', function() { - expect(function() { - cache.put('key', 'value', -100); - }).toThrow(); - }); + it('should throw an error given a non-function timeout callback', function() { + expect(function() { + cache.put('key', 'value', 100, 'foo'); + }).toThrow(); + }); - it('should throw an error given a non-function timeout callback', function() { - expect(function() { - cache.put('key', 'value', 100, 'foo'); - }).toThrow(); - }); + it('should cause the timeout callback to fire once the cache item expires', function() { + var callback = jasmine.createSpy('callback'); + cache.put('key', 'value', 1000, callback); + jasmine.clock().tick(999); + expect(callback).not.toHaveBeenCalled(); + jasmine.clock().tick(1); + expect(callback).toHaveBeenCalledWith('key'); + }); - it('should cause the timeout callback to fire once the cache item expires', function() { - var callback = jasmine.createSpy('callback'); - cache.put('key', 'value', 1000, callback); - jasmine.clock().tick(999); - expect(callback).not.toHaveBeenCalled(); - jasmine.clock().tick(1); - expect(callback).toHaveBeenCalledWith('key'); - }); + it('should override the timeout callback on a new put() with a different timeout callback', function() { + var spy1 = jasmine.createSpy(); + var spy2 = jasmine.createSpy(); + cache.put('key', 'value', 1000, spy1); + jasmine.clock().tick(999); + cache.put('key', 'value', 1000, spy2) + jasmine.clock().tick(1001); + expect(spy1).not.toHaveBeenCalled(); + expect(spy2).toHaveBeenCalledWith('key'); + }); - it('should override the timeout callback on a new put() with a different timeout callback', function() { - var spy1 = jasmine.createSpy(); - var spy2 = jasmine.createSpy(); - cache.put('key', 'value', 1000, spy1); - jasmine.clock().tick(999); - cache.put('key', 'value', 1000, spy2) - jasmine.clock().tick(1001); - expect(spy1).not.toHaveBeenCalled(); - expect(spy2).toHaveBeenCalledWith('key'); - }); + it('should cancel the timeout callback on a new put() without a timeout callback', function() { + var spy = jasmine.createSpy(); + cache.put('key', 'value', 1000, spy); + jasmine.clock().tick(999); + cache.put('key', 'value') + jasmine.clock().tick(1); + expect(spy).not.toHaveBeenCalled(); + }); - it('should cancel the timeout callback on a new put() without a timeout callback', function() { - var spy = jasmine.createSpy(); - cache.put('key', 'value', 1000, spy); - jasmine.clock().tick(999); - cache.put('key', 'value') - jasmine.clock().tick(1); - expect(spy).not.toHaveBeenCalled(); + it('should return the cached value', function() { + expect(cache.put('key', 'value')).toEqual('value'); + }); }); - it('should return the cached value', function() { - expect(cache.put('key', 'value')).toEqual('value'); - }); - }); + describe('del()', function() { + beforeEach(function() { + cache.setDebug(false); + }); - describe('del()', function() { - beforeEach(function() { - cache.setDebug(false); - }); + it('should return false given a key for an empty cache', function() { + expect(cache.del('miss')).toBe(false); + }); - it('should return false given a key for an empty cache', function() { - expect(cache.del('miss')).toBe(false); - }); + it('should return false given a key not in a non-empty cache', function() { + cache.put('key', 'value'); + expect(cache.del('miss')).toBe(false); + }); - it('should return false given a key not in a non-empty cache', function() { - cache.put('key', 'value'); - expect(cache.del('miss')).toBe(false); - }); + it('should return true given a key in the cache', function() { + cache.put('key', 'value'); + expect(cache.del('key')).toBe(true); + }); - it('should return true given a key in the cache', function() { - cache.put('key', 'value'); - expect(cache.del('key')).toBe(true); - }); + it('should remove the provided key from the cache', function() { + cache.put('key', 'value'); + expect(cache.get('key')).toEqual('value'); + expect(cache.del('key')).toBe(true); + expect(cache.get('key')).toBe(undefined); + }); - it('should remove the provided key from the cache', function() { - cache.put('key', 'value'); - expect(cache.get('key')).toEqual('value'); - expect(cache.del('key')).toBe(true); - expect(cache.get('key')).toBe(null); - }); + it('should decrement the cache size by 1', function() { + cache.put('key', 'value'); + expect(cache.size()).toEqual(1); + expect(cache.del('key')).toBe(true); + expect(cache.size()).toEqual(0); + }); - it('should decrement the cache size by 1', function() { - cache.put('key', 'value'); - expect(cache.size()).toEqual(1); - expect(cache.del('key')).toBe(true); - expect(cache.size()).toEqual(0); - }); + it('should not remove other keys in the cache', function() { + cache.put('key1', 'value1'); + cache.put('key2', 'value2'); + cache.put('key3', 'value3'); + expect(cache.get('key1')).toEqual('value1'); + expect(cache.get('key2')).toEqual('value2'); + expect(cache.get('key3')).toEqual('value3'); + cache.del('key1'); + expect(cache.get('key1')).toBe(undefined); + expect(cache.get('key2')).toEqual('value2'); + expect(cache.get('key3')).toEqual('value3'); + }); - it('should not remove other keys in the cache', function() { - cache.put('key1', 'value1'); - cache.put('key2', 'value2'); - cache.put('key3', 'value3'); - expect(cache.get('key1')).toEqual('value1'); - expect(cache.get('key2')).toEqual('value2'); - expect(cache.get('key3')).toEqual('value3'); - cache.del('key1'); - expect(cache.get('key1')).toBe(null); - expect(cache.get('key2')).toEqual('value2'); - expect(cache.get('key3')).toEqual('value3'); - }); + it('should only delete a key from the cache once even if called multiple times in a row', function() { + cache.put('key1', 'value1'); + cache.put('key2', 'value2'); + cache.put('key3', 'value3'); + expect(cache.size()).toEqual(3); + cache.del('key1'); + cache.del('key1'); + cache.del('key1'); + expect(cache.size()).toEqual(2); + }); - it('should only delete a key from the cache once even if called multiple times in a row', function() { - cache.put('key1', 'value1'); - cache.put('key2', 'value2'); - cache.put('key3', 'value3'); - expect(cache.size()).toEqual(3); - cache.del('key1'); - cache.del('key1'); - cache.del('key1'); - expect(cache.size()).toEqual(2); - }); + it('should handle deleting keys which were previously deleted and then re-added to the cache', function() { + cache.put('key', 'value'); + expect(cache.get('key')).toEqual('value'); + cache.del('key'); + expect(cache.get('key')).toBe(undefined); + cache.put('key', 'value'); + expect(cache.get('key')).toEqual('value'); + cache.del('key'); + expect(cache.get('key')).toBe(undefined); + }); - it('should handle deleting keys which were previously deleted and then re-added to the cache', function() { - cache.put('key', 'value'); - expect(cache.get('key')).toEqual('value'); - cache.del('key'); - expect(cache.get('key')).toBe(null); - cache.put('key', 'value'); - expect(cache.get('key')).toEqual('value'); - cache.del('key'); - expect(cache.get('key')).toBe(null); + it('should cancel the timeout callback for the deleted key', function() { + var spy = jasmine.createSpy(); + cache.put('key', 'value', 1000, spy); + cache.del('key'); + jasmine.clock().tick(1000); + expect(spy).not.toHaveBeenCalled(); + }); }); - it('should cancel the timeout callback for the deleted key', function() { - var spy = jasmine.createSpy(); - cache.put('key', 'value', 1000, spy); - cache.del('key'); - jasmine.clock().tick(1000); - expect(spy).not.toHaveBeenCalled(); - }); - }); + describe('clear()', function() { + beforeEach(function() { + cache.setDebug(false); + }); - describe('clear()', function() { - beforeEach(function() { - cache.setDebug(false); - }); + it('should have no effect given an empty cache', function() { + expect(cache.size()).toEqual(0); + cache.clear(); + expect(cache.size()).toEqual(0); + }); - it('should have no effect given an empty cache', function() { - expect(cache.size()).toEqual(0); - cache.clear(); - expect(cache.size()).toEqual(0); - }); + it('should remove all existing keys in the cache', function() { + cache.put('key1', 'value1'); + cache.put('key2', 'value2'); + cache.put('key3', 'value3'); + expect(cache.size()).toEqual(3); + cache.clear(); + expect(cache.size()).toEqual(0); + }); - it('should remove all existing keys in the cache', function() { - cache.put('key1', 'value1'); - cache.put('key2', 'value2'); - cache.put('key3', 'value3'); - expect(cache.size()).toEqual(3); - cache.clear(); - expect(cache.size()).toEqual(0); - }); + it('should remove the keys in the cache', function() { + cache.put('key1', 'value1'); + cache.put('key2', 'value2'); + cache.put('key3', 'value3'); + expect(cache.get('key1')).toEqual('value1'); + expect(cache.get('key2')).toEqual('value2'); + expect(cache.get('key3')).toEqual('value3'); + cache.clear(); + expect(cache.get('key1')).toBe(undefined); + expect(cache.get('key2')).toBe(undefined); + expect(cache.get('key3')).toBe(undefined); + }); - it('should remove the keys in the cache', function() { - cache.put('key1', 'value1'); - cache.put('key2', 'value2'); - cache.put('key3', 'value3'); - expect(cache.get('key1')).toEqual('value1'); - expect(cache.get('key2')).toEqual('value2'); - expect(cache.get('key3')).toEqual('value3'); - cache.clear(); - expect(cache.get('key1')).toBe(null); - expect(cache.get('key2')).toBe(null); - expect(cache.get('key3')).toBe(null); - }); + it('should reset the cache size to 0', function() { + cache.put('key1', 'value1'); + cache.put('key2', 'value2'); + cache.put('key3', 'value3'); + expect(cache.size()).toEqual(3); + cache.clear(); + expect(cache.size()).toEqual(0); + }); - it('should reset the cache size to 0', function() { - cache.put('key1', 'value1'); - cache.put('key2', 'value2'); - cache.put('key3', 'value3'); - expect(cache.size()).toEqual(3); - cache.clear(); - expect(cache.size()).toEqual(0); - }); + it('should reset the debug cache hits', function() { + cache.setDebug(false); + cache.put('key', 'value'); + cache.get('key'); + expect(cache.hits()).toEqual(1); + cache.clear(); + expect(cache.hits()).toEqual(0); + }); - it('should reset the debug cache hits', function() { - cache.setDebug(false); - cache.put('key', 'value'); - cache.get('key'); - expect(cache.hits()).toEqual(1); - cache.clear(); - expect(cache.hits()).toEqual(0); - }); + it('should reset the debug cache misses', function() { + cache.setDebug(false); + cache.put('key', 'value'); + cache.get('miss1'); + expect(cache.misses()).toEqual(1); + cache.clear(); + expect(cache.misses()).toEqual(0); + }); - it('should reset the debug cache misses', function() { - cache.setDebug(false); - cache.put('key', 'value'); - cache.get('miss1'); - expect(cache.misses()).toEqual(1); - cache.clear(); - expect(cache.misses()).toEqual(0); + it('should cancel the timeout callbacks for all existing keys', function() { + var spy1 = jasmine.createSpy(); + var spy2 = jasmine.createSpy(); + var spy3 = jasmine.createSpy(); + cache.put('key1', 'value1', 1000, spy1); + cache.put('key2', 'value2', 1000, spy2); + cache.put('key3', 'value3', 1000, spy3); + cache.clear(); + jasmine.clock().tick(1000); + expect(spy1).not.toHaveBeenCalled(); + expect(spy2).not.toHaveBeenCalled(); + expect(spy3).not.toHaveBeenCalled(); + }); }); - it('should cancel the timeout callbacks for all existing keys', function() { - var spy1 = jasmine.createSpy(); - var spy2 = jasmine.createSpy(); - var spy3 = jasmine.createSpy(); - cache.put('key1', 'value1', 1000, spy1); - cache.put('key2', 'value2', 1000, spy2); - cache.put('key3', 'value3', 1000, spy3); - cache.clear(); - jasmine.clock().tick(1000); - expect(spy1).not.toHaveBeenCalled(); - expect(spy2).not.toHaveBeenCalled(); - expect(spy3).not.toHaveBeenCalled(); - }); - }); + describe('get()', function() { + beforeEach(function() { + cache.setDebug(false); + }); - describe('get()', function() { - beforeEach(function() { - cache.setDebug(false); - }); + it('should return null given a key for an empty cache', function() { + expect(cache.get('miss')).toBe(undefined); + }); - it('should return null given a key for an empty cache', function() { - expect(cache.get('miss')).toBe(null); - }); + it('should return null given a key not in a non-empty cache', function() { + cache.put('key', 'value'); + expect(cache.get('miss')).toBe(undefined); + }); - it('should return null given a key not in a non-empty cache', function() { - cache.put('key', 'value'); - expect(cache.get('miss')).toBe(null); - }); + it('should return the corresponding value of a key in the cache', function() { + cache.put('key', 'value'); + expect(cache.get('key')).toEqual('value'); + }); - it('should return the corresponding value of a key in the cache', function() { - cache.put('key', 'value'); - expect(cache.get('key')).toEqual('value'); - }); + it('should return the latest corresponding value of a key in the cache', function() { + cache.put('key', 'value1'); + cache.put('key', 'value2'); + cache.put('key', 'value3'); + expect(cache.get('key')).toEqual('value3'); + }); - it('should return the latest corresponding value of a key in the cache', function() { - cache.put('key', 'value1'); - cache.put('key', 'value2'); - cache.put('key', 'value3'); - expect(cache.get('key')).toEqual('value3'); - }); + it('should handle various types of cache keys', function() { + var keys = [null, undefined, NaN, true, false, 0, 1, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY, '', 'a', [], {}, [1, 'a', false], {a:1,b:'a',c:false}, function() {}]; + keys.forEach(function(key, index) { + var value = 'value' + index; + cache.put(key, value); + expect(cache.get(key)).toDeepEqual(value); + }); + }); - it('should handle various types of cache keys', function() { - var keys = [null, undefined, NaN, true, false, 0, 1, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY, '', 'a', [], {}, [1, 'a', false], {a:1,b:'a',c:false}, function() {}]; - keys.forEach(function(key, index) { - var value = 'value' + index; - cache.put(key, value); - expect(cache.get(key)).toDeepEqual(value); - }); - }); + it('should handle various types of cache values', function() { + var values = [null, undefined, NaN, true, false, 0, 1, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY, '', 'a', [], {}, [1, 'a', false], {a:1,b:'a',c:false}, function() {}]; + values.forEach(function(value, index) { + var key = 'key' + index; + cache.put(key, value); + expect(cache.get(key)).toDeepEqual(value); + }); + }); - it('should handle various types of cache values', function() { - var values = [null, undefined, NaN, true, false, 0, 1, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY, '', 'a', [], {}, [1, 'a', false], {a:1,b:'a',c:false}, function() {}]; - values.forEach(function(value, index) { - var key = 'key' + index; - cache.put(key, value); - expect(cache.get(key)).toDeepEqual(value); - }); - }); + it('should set a default timeout given no expiration time', function() { + cache.put('key', 'value'); + jasmine.clock().tick(cache.defaultTtl + 1); + expect(cache.get('key')).toEqual(undefined); + }); - it('should not set a timeout given no expiration time', function() { - cache.put('key', 'value'); - jasmine.clock().tick(1000); - expect(cache.get('key')).toEqual('value'); - }); + it('should not timeout if the expiration is set to infinity', function() { + cache.put('key', 'value', Infinity); + jasmine.clock().tick(100000); + expect(cache.get('key')).toEqual('value'); + }); - it('should return the corresponding value of a non-expired key in the cache', function() { - cache.put('key', 'value', 1000); - jasmine.clock().tick(999); - expect(cache.get('key')).toEqual('value'); - }); + it('should return the corresponding value of a non-expired key in the cache', function() { + cache.put('key', 'value', 1000); + jasmine.clock().tick(999); + expect(cache.get('key')).toEqual('value'); + }); - it('should return null given an expired key', function() { - cache.put('key', 'value', 1000); - jasmine.clock().tick(1000); - expect(cache.get('key')).toBe(null); - }); + it('should return null given an expired key', function() { + cache.put('key', 'value', 1000); + jasmine.clock().tick(1000); + expect(cache.get('key')).toBe(undefined); + }); - it('should delete an object which has expired and is still in the cache', function() { - cache.setDebug(false); - cache.put('key', 'value', 10000); - cache.killTimer('key'); - jasmine.clock().tick(10001); - expect(cache.keys()).toDeepEqual(['key']); - cache.get('key'); - expect(cache.keys()).toDeepEqual([]); - }); + it('should delete an object which has expired and is still in the cache', function() { + cache.setDebug(false); + cache.put('key', 'value', 10000); + cache.killTimer('key'); + jasmine.clock().tick(10001); + expect(cache.keys()).toDeepEqual(['key']); + cache.get('key'); + expect(cache.keys()).toDeepEqual([]); + }); - it('should return null given a key which is a property on the Object prototype', function() { - expect(cache.get('toString')).toBe(null); - }); + it('should return null given a key which is a property on the Object prototype', function() { + expect(cache.get('toString')).toBe(undefined); + }); - it('should allow reading the value for a key which is a property on the Object prototype', function() { - cache.put('toString', 'value'); - expect(cache.get('toString')).toEqual('value'); + it('should allow reading the value for a key which is a property on the Object prototype', function() { + cache.put('toString', 'value'); + expect(cache.get('toString')).toEqual('value'); + }); }); - }); describe("killTimer()", function() { it("should prevent a timer from being executed", function() { @@ -364,264 +370,262 @@ describe('MemoryCache', function() { }); }); - describe('size()', function() { - beforeEach(function() { - cache.setDebug(false); - }); + describe('size()', function() { + beforeEach(function() { + cache.setDebug(false); + }); - it('should return 0 given a fresh cache', function() { - expect(cache.size()).toEqual(0); - }); + it('should return 0 given a fresh cache', function() { + expect(cache.size()).toEqual(0); + }); - it('should return 1 after adding a single item to the cache', function() { - cache.put('key', 'value'); - expect(cache.size()).toEqual(1); - }); + it('should return 1 after adding a single item to the cache', function() { + cache.put('key', 'value'); + expect(cache.size()).toEqual(1); + }); - it('should return 3 after adding three items to the cache', function() { - cache.put('key1', 'value1'); - cache.put('key2', 'value2'); - cache.put('key3', 'value3'); - expect(cache.size()).toEqual(3); - }); + it('should return 3 after adding three items to the cache', function() { + cache.put('key1', 'value1'); + cache.put('key2', 'value2'); + cache.put('key3', 'value3'); + expect(cache.size()).toEqual(3); + }); - it('should not multi-count duplicate items added to the cache', function() { - cache.put('key', 'value1'); - expect(cache.size()).toEqual(1); - cache.put('key', 'value2'); - expect(cache.size()).toEqual(1); - }); + it('should not multi-count duplicate items added to the cache', function() { + cache.put('key', 'value1'); + expect(cache.size()).toEqual(1); + cache.put('key', 'value2'); + expect(cache.size()).toEqual(1); + }); - it('should update when a key in the cache expires', function() { - cache.put('key', 'value', 1000); - expect(cache.size()).toEqual(1); - jasmine.clock().tick(999); - expect(cache.size()).toEqual(1); - jasmine.clock().tick(1); - expect(cache.size()).toEqual(0); + it('should update when a key in the cache expires', function() { + cache.put('key', 'value', 1000); + expect(cache.size()).toEqual(1); + jasmine.clock().tick(999); + expect(cache.size()).toEqual(1); + jasmine.clock().tick(1); + expect(cache.size()).toEqual(0); + }); }); - }); - describe('debug()', function() { - it('should change the value of the debug property', function() { - expect(cache.debug).toEqual(false); - cache.setDebug(true); - expect(cache.debug).toEqual(true); + describe('debug()', function() { + it('should change the value of the debug property', function() { + expect(cache.debug).toEqual(false); + cache.setDebug(true); + expect(cache.debug).toEqual(true); + }); }); - }); - describe('hits()', function() { - beforeEach(function() { - cache.setDebug(false); - }); + describe('hits()', function() { + beforeEach(function() { + cache.setDebug(false); + }); - it('should return 0 given an empty cache', function() { - expect(cache.hits()).toEqual(0); - }); + it('should return 0 given an empty cache', function() { + expect(cache.hits()).toEqual(0); + }); - it('should return 0 given a non-empty cache which has not been accessed', function() { - cache.put('key', 'value'); - expect(cache.hits()).toEqual(0); - }); + it('should return 0 given a non-empty cache which has not been accessed', function() { + cache.put('key', 'value'); + expect(cache.hits()).toEqual(0); + }); - it('should return 0 given a non-empty cache which has had only misses', function() { - cache.put('key', 'value'); - cache.get('miss1'); - cache.get('miss2'); - cache.get('miss3'); - expect(cache.hits()).toEqual(0); - }); + it('should return 0 given a non-empty cache which has had only misses', function() { + cache.put('key', 'value'); + cache.get('miss1'); + cache.get('miss2'); + cache.get('miss3'); + expect(cache.hits()).toEqual(0); + }); - it('should return 1 given a non-empty cache which has had a single hit', function() { - cache.put('key', 'value'); - cache.get('key'); - expect(cache.hits()).toEqual(1); - }); + it('should return 1 given a non-empty cache which has had a single hit', function() { + cache.put('key', 'value'); + cache.get('key'); + expect(cache.hits()).toEqual(1); + }); - it('should return 3 given a non-empty cache which has had three hits on the same key', function() { - cache.put('key', 'value'); - cache.get('key'); - cache.get('key'); - cache.get('key'); - expect(cache.hits()).toEqual(3); - }); + it('should return 3 given a non-empty cache which has had three hits on the same key', function() { + cache.put('key', 'value'); + cache.get('key'); + cache.get('key'); + cache.get('key'); + expect(cache.hits()).toEqual(3); + }); - it('should return 3 given a non-empty cache which has had three hits across many keys', function() { - cache.put('key1', 'value1'); - cache.put('key2', 'value2'); - cache.put('key3', 'value3'); - cache.get('key1'); - cache.get('key2'); - cache.get('key3'); - expect(cache.hits()).toEqual(3); - }); + it('should return 3 given a non-empty cache which has had three hits across many keys', function() { + cache.put('key1', 'value1'); + cache.put('key2', 'value2'); + cache.put('key3', 'value3'); + cache.get('key1'); + cache.get('key2'); + cache.get('key3'); + expect(cache.hits()).toEqual(3); + }); - it('should return the correct value after a sequence of hits and misses', function() { - cache.put('key1', 'value1'); - cache.put('key2', 'value2'); - cache.put('key3', 'value3'); - cache.get('key1'); - cache.get('miss'); - cache.get('key3'); - expect(cache.hits()).toEqual(2); - }); + it('should return the correct value after a sequence of hits and misses', function() { + cache.put('key1', 'value1'); + cache.put('key2', 'value2'); + cache.put('key3', 'value3'); + cache.get('key1'); + cache.get('miss'); + cache.get('key3'); + expect(cache.hits()).toEqual(2); + }); - it('should not count hits for expired keys', function() { - cache.put('key', 'value', 1000); - cache.get('key'); - expect(cache.hits()).toEqual(1); - jasmine.clock().tick(999); - cache.get('key'); - expect(cache.hits()).toEqual(2); - jasmine.clock().tick(1); - cache.get('key'); - expect(cache.hits()).toEqual(2); + it('should not count hits for expired keys', function() { + cache.put('key', 'value', 1000); + cache.get('key'); + expect(cache.hits()).toEqual(1); + jasmine.clock().tick(999); + cache.get('key'); + expect(cache.hits()).toEqual(2); + jasmine.clock().tick(1); + cache.get('key'); + expect(cache.hits()).toEqual(2); + }); }); - }); - describe('misses()', function() { - beforeEach(function() { - cache.setDebug(false); - }); + describe('misses()', function() { + beforeEach(function() { + cache.setDebug(false); + }); - it('should return 0 given an empty cache', function() { - expect(cache.misses()).toEqual(0); - }); + it('should return 0 given an empty cache', function() { + expect(cache.misses()).toEqual(0); + }); - it('should return 0 given a non-empty cache which has not been accessed', function() { - cache.put('key', 'value'); - expect(cache.misses()).toEqual(0); - }); + it('should return 0 given a non-empty cache which has not been accessed', function() { + cache.put('key', 'value'); + expect(cache.misses()).toEqual(0); + }); - it('should return 0 given a non-empty cache which has had only hits', function() { - cache.put('key', 'value'); - cache.get('key'); - cache.get('key'); - cache.get('key'); - expect(cache.misses()).toEqual(0); - }); + it('should return 0 given a non-empty cache which has had only hits', function() { + cache.put('key', 'value'); + cache.get('key'); + cache.get('key'); + cache.get('key'); + expect(cache.misses()).toEqual(0); + }); - it('should return 1 given a non-empty cache which has had a single miss', function() { - cache.put('key', 'value'); - cache.get('miss'); - expect(cache.misses()).toEqual(1); - }); + it('should return 1 given a non-empty cache which has had a single miss', function() { + cache.put('key', 'value'); + cache.get('miss'); + expect(cache.misses()).toEqual(1); + }); - it('should return 3 given a non-empty cache which has had three misses', function() { - cache.put('key', 'value'); - cache.get('miss1'); - cache.get('miss2'); - cache.get('miss3'); - expect(cache.misses()).toEqual(3); - }); + it('should return 3 given a non-empty cache which has had three misses', function() { + cache.put('key', 'value'); + cache.get('miss1'); + cache.get('miss2'); + cache.get('miss3'); + expect(cache.misses()).toEqual(3); + }); - it('should return the correct value after a sequence of hits and misses', function() { - cache.put('key1', 'value1'); - cache.put('key2', 'value2'); - cache.put('key3', 'value3'); - cache.get('key1'); - cache.get('miss'); - cache.get('key3'); - expect(cache.misses()).toEqual(1); - }); + it('should return the correct value after a sequence of hits and misses', function() { + cache.put('key1', 'value1'); + cache.put('key2', 'value2'); + cache.put('key3', 'value3'); + cache.get('key1'); + cache.get('miss'); + cache.get('key3'); + expect(cache.misses()).toEqual(1); + }); - it('should count misses for expired keys', function() { - cache.put('key', 'value', 1000); - cache.get('key'); - expect(cache.misses()).toEqual(0); - jasmine.clock().tick(999); - cache.get('key'); - expect(cache.misses()).toEqual(0); - jasmine.clock().tick(1); - cache.get('key'); - expect(cache.misses()).toEqual(1); + it('should count misses for expired keys', function() { + cache.put('key', 'value', 1000); + cache.get('key'); + expect(cache.misses()).toEqual(0); + jasmine.clock().tick(999); + cache.get('key'); + expect(cache.misses()).toEqual(0); + jasmine.clock().tick(1); + cache.get('key'); + expect(cache.misses()).toEqual(1); + }); }); - }); - describe('keys()', function() { - beforeEach(function() { - cache.setDebug(false); - }); + describe('keys()', function() { + beforeEach(function() { + cache.setDebug(false); + }); - it('should return an empty array given an empty cache', function() { - expect(cache.keys()).toDeepEqual([]); - }); + it('should return an empty array given an empty cache', function() { + expect(cache.keys()).toDeepEqual([]); + }); - it('should return a single key after adding a single item to the cache', function() { - cache.put('key', 'value'); - expect(cache.keys()).toDeepEqual(['key']); - }); + it('should return a single key after adding a single item to the cache', function() { + cache.put('key', 'value'); + expect(cache.keys()).toDeepEqual(['key']); + }); - it('should return 3 keys after adding three items to the cache', function() { - cache.put('key1', 'value1'); - cache.put('key2', 'value2'); - cache.put('key3', 'value3'); - expect(cache.keys()).toDeepEqual(['key1', 'key2', 'key3']); - }); + it('should return 3 keys after adding three items to the cache', function() { + cache.put('key1', 'value1'); + cache.put('key2', 'value2'); + cache.put('key3', 'value3'); + expect(cache.keys()).toDeepEqual(['key1', 'key2', 'key3']); + }); - it('should not multi-count duplicate items added to the cache', function() { - cache.put('key', 'value1'); - expect(cache.keys()).toDeepEqual(['key']); - cache.put('key', 'value2'); - expect(cache.keys()).toDeepEqual(['key']); - }); + it('should not multi-count duplicate items added to the cache', function() { + cache.put('key', 'value1'); + expect(cache.keys()).toDeepEqual(['key']); + cache.put('key', 'value2'); + expect(cache.keys()).toDeepEqual(['key']); + }); - it('should update when a key in the cache expires', function() { - cache.put('key', 'value', 1000); - expect(cache.keys()).toDeepEqual(['key']); - jasmine.clock().tick(999); - expect(cache.keys()).toDeepEqual(['key']); - jasmine.clock().tick(1); - expect(cache.keys()).toDeepEqual([]); + it('should update when a key in the cache expires', function() { + cache.put('key', 'value', 1000); + expect(cache.keys()).toDeepEqual(['key']); + jasmine.clock().tick(999); + expect(cache.keys()).toDeepEqual(['key']); + jasmine.clock().tick(1); + expect(cache.keys()).toDeepEqual([]); + }); }); - }); - describe('toArray()', function() { - beforeEach(function() { - cache.setDebug(false); - }); + describe('toArray()', function() { + beforeEach(function() { + cache.setDebug(false); + }); - it("should return an array of values", function() { - cache.put('key1', 'value1'); - cache.put('key2', 'value2'); - expect(cache.toArray()).toDeepEqual([ - { expire: NaN, value: 'value1' }, - { expire: NaN, value: 'value2' } - ]); + it("should return an array of values", function() { + cache.put('key1', 'value1'); + cache.put('key2', 'value2'); + expect( + cache.toArray() + .map(function(item) { return item.value }) + ).toDeepEqual(['value1', 'value2']); + }); }); - }); - describe('filter()', function() { - beforeEach(function() { - cache.setDebug(false); - }); + describe('filter()', function() { + beforeEach(function() { + cache.setDebug(false); + }); - it("should filter based on a predicate", function() { - cache.put('key1', 'value1'); - cache.put('key2', 'value2'); - var filtered = cache.filter(function(item) { - return item.value == 'value1'; + it("should filter based on a predicate", function() { + cache.put('key1', 'value1'); + cache.put('key2', 'value2'); + var filtered = cache.filter(function(item) { + return item.value == 'value1'; + }); + expect(filtered.get('key1').value).toEqual('value1'); + expect(filtered.get('key2')).toEqual(undefined); }); - expect(filtered.get('key1')).toDeepEqual({ expire: NaN, value: 'value1' }); - expect(filtered.get('key2')).toEqual(undefined); - }); - it("should filter all keys without expirations", function() { - cache.put('key1', 'value1'); - cache.put('key2', 'value2'); - cache.put('key3', 'value3', 10000); - cache.put('key4', 'value4', 20000); - var filtered = cache.filter(function(item) { - return isNaN(item.expire); - }) - expect(filtered.get('key1')).toDeepEqual({ expire: NaN, value: 'value1' }); - expect(filtered.get('key2')).toDeepEqual({ expire: NaN, value: 'value2' }); - expect(filtered.get('key3')).toEqual(undefined); - expect(filtered.get('key4')).toEqual(undefined); + it("should filter all keys without expirations", function() { + cache.put('key1', 'value1', Infinity); + cache.put('key2', 'value2', Infinity); + cache.put('key3', 'value3', 10000); + cache.put('key4', 'value4', 20000); + var filtered = cache.filter(function(item) { + return !item.timeout; + }) + expect(filtered.get('key1').value).toEqual('value1'); + expect(filtered.get('key2').value).toEqual('value2'); + expect(filtered.get('key3')).toEqual(undefined); + expect(filtered.get('key4')).toEqual(undefined); + }); }); - }); - - describe("map()", function() { it("should map the values of the cache", function() { @@ -633,10 +637,10 @@ describe('MemoryCache', function() { value.value = value.value + 1; return value; }); - expect(mapped.get('key1')).toDeepEqual({ expire: NaN, value: 2 }); - expect(mapped.get('key2')).toDeepEqual({ expire: NaN, value: 3 }); - expect(mapped.get('key3')).toDeepEqual({ expire: NaN, value: 4 }); - expect(mapped.get('key4')).toDeepEqual({ expire: NaN, value: 5 }); + expect(mapped.get('key1').value).toEqual(2); + expect(mapped.get('key2').value).toEqual(3); + expect(mapped.get('key3').value).toEqual(4); + expect(mapped.get('key4').value).toEqual(5); }) }) }); diff --git a/src/index.js b/src/index.js index ec2dd1bb15..4cf2fedfc4 100644 --- a/src/index.js +++ b/src/index.js @@ -52,8 +52,6 @@ function ParseServer(args) { var cache = this.cacheProvider.getAdapter(); - console.log(cache); - if (args.databaseAdapter) { DatabaseAdapter.setAdapter(args.databaseAdapter); } @@ -93,7 +91,7 @@ function ParseServer(args) { appInfo['facebookAppIds'].push(process.env.FACEBOOK_APP_ID); } - cache.put(args.appId, appInfo); + cache.put(args.appId, appInfo, Infinity); // Initialize the node client SDK automatically Parse.initialize(args.appId, args.javascriptKey || '', args.masterKey); @@ -181,13 +179,31 @@ function getClassName(parseClass) { return parseClass; } +function resolveAdapter(adapter, options) { + // Support passing in adapter paths + if (typeof adapter === 'string') { + adapter = require(adapter); + } + + // Instantiate the adapter if the class got passed instead of an instance + if (typeof adapter === 'function') { + adapter = new adapter(options); + } + + return adapter; +} + // TODO: Add configurable TTLs for cache entries function setupCache(config) { config = config || {}; - this.cacheProvider = new CacheProvider(config.adapter || DefaultCacheAdapter); + config.adapter = config.adapter || DefaultCacheAdapter; + + var adapter = this.resolveAdapter(config.adapter, config.options); + this.cacheProvider = new CacheProvider(adapter); } ParseServer.prototype.setupCache = setupCache; +ParseServer.prototype.resolveAdapter = resolveAdapter; module.exports = { ParseServer: ParseServer, diff --git a/src/testing-routes.js b/src/testing-routes.js index 03d8bb32c6..4ad7c416b0 100644 --- a/src/testing-routes.js +++ b/src/testing-routes.js @@ -14,7 +14,7 @@ function createApp(req, res) { cache.put(appId, { 'collectionPrefix': appId + '_', 'masterKey': 'master' - }); + }, Infinity); var keys = { 'application_id': appId, From 127bdb742654d2851c622c1988f04a395f297111 Mon Sep 17 00:00:00 2001 From: Alexander Mays Date: Tue, 9 Feb 2016 00:03:12 -0500 Subject: [PATCH 04/14] Gave up my reservations about relying on Node module caching to enforce the singleton pattern. The correct way is to not use the constructor but rather a 'setup', 'init' or other method. Signed-off-by: Alexander Mays --- classes/BaseProvider.js | 7 +++++-- classes/CacheProvider.js | 17 +++++------------ interfaces/ServiceProvider.js | 2 +- src/Auth.js | 3 +-- src/Config.js | 3 +-- src/DatabaseAdapter.js | 3 +-- src/index.js | 3 ++- src/middlewares.js | 3 +-- src/rest.js | 3 +-- src/testing-routes.js | 3 +-- 10 files changed, 19 insertions(+), 28 deletions(-) diff --git a/classes/BaseProvider.js b/classes/BaseProvider.js index 856646af06..f70e1007d0 100644 --- a/classes/BaseProvider.js +++ b/classes/BaseProvider.js @@ -2,7 +2,9 @@ var ServiceProviderInterface = require('../interfaces/ServiceProvider'); var util = require('util'); function BaseProvider(adapter) { - this.adapter = adapter; + if (adapter) { + this.adapter = adapter; + } }; util.inherits(BaseProvider, ServiceProviderInterface); @@ -17,5 +19,6 @@ function setAdapter(adapter) { BaseProvider.prototype.getAdapter = getAdapter; BaseProvider.prototype.setAdapter = setAdapter; +BaseProvider.prototype.BaseProvider = BaseProvider; -module.exports = BaseProvider; \ No newline at end of file +exports = module.exports = new BaseProvider(); \ No newline at end of file diff --git a/classes/CacheProvider.js b/classes/CacheProvider.js index 5131ea0fce..463dc8c6a0 100644 --- a/classes/CacheProvider.js +++ b/classes/CacheProvider.js @@ -1,19 +1,12 @@ -var BaseProvider = require('./BaseProvider'); +var BaseProvider = require('./BaseProvider').BaseProvider; var util = require('util'); -// Singleton for the entire server. -// TODO: Refactor away from singleton paradigm -var instance = null; - function CacheProvider(adapter) { - if (instance) { - return instance; - } - - instance = this; - this.adapter = adapter; + CacheProvider.super_.call(this) }; util.inherits(CacheProvider, BaseProvider); -module.exports = CacheProvider; \ No newline at end of file +CacheProvider.prototype.CacheProvider = CacheProvider; + +exports = module.exports = new CacheProvider(); \ No newline at end of file diff --git a/interfaces/ServiceProvider.js b/interfaces/ServiceProvider.js index c73c5f450c..808ddd9e30 100644 --- a/interfaces/ServiceProvider.js +++ b/interfaces/ServiceProvider.js @@ -9,4 +9,4 @@ ServiceProviderInterface.prototype.setAdapter = function() { throw new Error('A service provider must implement setAdapter!'); } -module.exports = ServiceProviderInterface; \ No newline at end of file +exports = module.exports = ServiceProviderInterface; \ No newline at end of file diff --git a/src/Auth.js b/src/Auth.js index 4b5b64ff96..04c0fde0ef 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -41,8 +41,7 @@ function nobody(config) { // Returns a promise that resolves to an Auth object var getAuthForSessionToken = function(config, sessionToken) { - var cacheProvider = new (require('./classes/CacheProvider')); - var cache = cacheProvider.getAdapter(); + var cache = require('./classes/CacheProvider').getAdapter(); var cachedUser = cache.get(sessionToken); if (cachedUser) { diff --git a/src/Config.js b/src/Config.js index 3ab9eec4f4..e125857dcc 100644 --- a/src/Config.js +++ b/src/Config.js @@ -2,8 +2,7 @@ // configured. // mount is the URL for the root of the API; includes http, domain, etc. function Config(applicationId, mount) { - var cacheProvider = new (require('./classes/CacheProvider')); - var cache = cacheProvider.getAdapter(); + var cache = require('./classes/CacheProvider').getAdapter(); var DatabaseAdapter = require('./DatabaseAdapter'); var cacheInfo = cache.get(applicationId); diff --git a/src/DatabaseAdapter.js b/src/DatabaseAdapter.js index 81891d968d..2b54b756c0 100644 --- a/src/DatabaseAdapter.js +++ b/src/DatabaseAdapter.js @@ -34,8 +34,7 @@ function setAppDatabaseURI(appId, uri) { } function getDatabaseConnection(appId) { - var cacheProvider = new (require('./classes/CacheProvider')); - var cache = cacheProvider.getAdapter(); + var cache = require('./classes/CacheProvider').getAdapter(); if (dbConnections[appId]) { return dbConnections[appId]; } diff --git a/src/index.js b/src/index.js index 4cf2fedfc4..df63eebb0e 100644 --- a/src/index.js +++ b/src/index.js @@ -199,7 +199,8 @@ function setupCache(config) { config.adapter = config.adapter || DefaultCacheAdapter; var adapter = this.resolveAdapter(config.adapter, config.options); - this.cacheProvider = new CacheProvider(adapter); + CacheProvider.setAdapter(adapter); + this.cacheProvider = CacheProvider; } ParseServer.prototype.setupCache = setupCache; diff --git a/src/middlewares.js b/src/middlewares.js index ba9c52cf28..882c029e77 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -10,8 +10,7 @@ var Config = require('./Config'); // req.config - the Config for this app // req.auth - the Auth for this request function handleParseHeaders(req, res, next) { - var cacheProvider = new (require('./classes/CacheProvider')); - var cache = cacheProvider.getAdapter(); + var cache = require('./classes/CacheProvider').getAdapter(); var mountPathLength = req.originalUrl.length - req.url.length; var mountPath = req.originalUrl.slice(0, mountPathLength); var mount = req.protocol + '://' + req.get('host') + mountPath; diff --git a/src/rest.js b/src/rest.js index 13f2ff2e78..a9f45deaa0 100644 --- a/src/rest.js +++ b/src/rest.js @@ -35,8 +35,7 @@ function del(config, auth, className, objectId) { enforceRoleSecurity('delete', className, auth); var inflatedObject; - var cacheProvider = new (require('./classes/CacheProvider')); - var cache = cacheProvider.getAdapter(); + var cache = require('./classes/CacheProvider').getAdapter(); return Promise.resolve().then(() => { if (triggers.getTrigger(className, 'beforeDelete') || diff --git a/src/testing-routes.js b/src/testing-routes.js index 4ad7c416b0..d965ca2f45 100644 --- a/src/testing-routes.js +++ b/src/testing-routes.js @@ -8,8 +8,7 @@ var router = express.Router(); // creates a unique app in the cache, with a collection prefix function createApp(req, res) { - var cacheProvider = new (require('./classes/CacheProvider')); - var cache = cacheProvider.getAdapter(); + var cache = require('./classes/CacheProvider').getAdapter(); var appId = rack(); cache.put(appId, { 'collectionPrefix': appId + '_', From c853957e325b938298ad9b0193089e587217f71b Mon Sep 17 00:00:00 2001 From: Alexander Mays Date: Tue, 9 Feb 2016 01:17:00 -0500 Subject: [PATCH 05/14] Clean up, added documentation. Moved resolveAdapter to the provider Signed-off-by: Alexander Mays --- classes/BaseProvider.js | 42 +++++++++++++++++++- classes/CacheProvider.js | 28 ++++++++++++- classes/MemoryCache.js | 75 +++++++++++++++++++++++++++++++++++ interfaces/ServiceProvider.js | 32 +++++++++++++-- src/Auth.js | 3 +- src/index.js | 41 ++++--------------- src/middlewares.js | 4 +- src/rest.js | 3 +- src/testing-routes.js | 3 +- 9 files changed, 185 insertions(+), 46 deletions(-) diff --git a/classes/BaseProvider.js b/classes/BaseProvider.js index f70e1007d0..b61096d110 100644 --- a/classes/BaseProvider.js +++ b/classes/BaseProvider.js @@ -1,6 +1,13 @@ var ServiceProviderInterface = require('../interfaces/ServiceProvider'); var util = require('util'); +/** + * A base provider class that allows for an abstraction of adapter implementations + * + * @class + * @implements {ServiceProvider} + * @param {Object} adapter - An adapter + */ function BaseProvider(adapter) { if (adapter) { this.adapter = adapter; @@ -9,16 +16,47 @@ function BaseProvider(adapter) { util.inherits(BaseProvider, ServiceProviderInterface); +/** + * Get the adapter + * + * @returns {Object} An adapter instance + */ function getAdapter() { return this.adapter; } +/** + * Set the adapter + * + * @param {Object} adapter - An adapter + */ function setAdapter(adapter) { this.adapter = adapter; } +/** + * Resolves the adapter + * + * @param {Object|String|Function} adapter - [1] An object implementing the adapter interface, or [2] a function that returns [1], or [3] A string of either the name of an included npm module or a path to a local module that returns [1] or [2]. + * @param {Object} options - An object passed to the adapter on instantiation (if adapter is not already instantiated) + * @returns {Object} An object implementing the adapter interface + */ +function resolveAdapter(adapter, options) { + // Support passing in adapter paths + if (typeof adapter === 'string') { + adapter = require(adapter); + } + + // Instantiate the adapter if the class got passed instead of an instance + if (typeof adapter === 'function') { + adapter = new adapter(options); + } + + return adapter; +} + BaseProvider.prototype.getAdapter = getAdapter; BaseProvider.prototype.setAdapter = setAdapter; -BaseProvider.prototype.BaseProvider = BaseProvider; +BaseProvider.prototype.resolveAdapter = resolveAdapter; -exports = module.exports = new BaseProvider(); \ No newline at end of file +exports = module.exports = BaseProvider; \ No newline at end of file diff --git a/classes/CacheProvider.js b/classes/CacheProvider.js index 463dc8c6a0..7618e74941 100644 --- a/classes/CacheProvider.js +++ b/classes/CacheProvider.js @@ -1,12 +1,38 @@ -var BaseProvider = require('./BaseProvider').BaseProvider; var util = require('util'); +var BaseProvider = require('./BaseProvider'); +var DefaultCacheAdapter = require('./MemoryCache'); + +/** +* Abstract class the provides a reference to an adapter instance (a caching implementation) +* +* @class +* @extends {BaseProvider} +* @param {Object} adapter - A cache adapter +*/ function CacheProvider(adapter) { CacheProvider.super_.call(this) }; +/** +* Setup the cache provider given a configuration object +* +* @method +* @param {Object} config - A configuration object +* @param {Any} config.adapter - A string, object, instance, or function that resolves to an adapter implementation +* @param {Object} config.options - An object passed to the adapter on instantiation (if adapter is not already instantiated) +*/ +function setup (config) { + config = config || {}; + config.adapter = config.adapter || DefaultCacheAdapter; + + var adapter = this.resolveAdapter(config.adapter, config.options); + this.setAdapter(adapter); +} + util.inherits(CacheProvider, BaseProvider); +CacheProvider.prototype.setup = setup; CacheProvider.prototype.CacheProvider = CacheProvider; exports = module.exports = new CacheProvider(); \ No newline at end of file diff --git a/classes/MemoryCache.js b/classes/MemoryCache.js index c43e31898c..f2ae43f3dd 100644 --- a/classes/MemoryCache.js +++ b/classes/MemoryCache.js @@ -1,5 +1,13 @@ 'use strict'; // Modified from https://github.com/ptarjan/node-cache/blob/master/index.js + +/** +* Creates a new in-memory cache using Map for storage +* +* @class +* @param {Object} options - An object of default options +* @param {String} [options.defaultTtl=600000] - The number of milliseconds to use as the default time-to-live of a cache entry +*/ function MemoryCache(options) { options = options || {}; @@ -10,6 +18,15 @@ function MemoryCache(options) { this.defaultTtl = options.defaultTtl || 10 * 60 * 1000; }; +/** + * Puts a key value mapping into the map that will automatically expire given a TTL. + * @method put + * @param {String} key - A unique key + * @param {Any} value - A value to be stored + * @param {Number} ttl - The number of milliseconds until the key/value pair is removed from the cache + * @param {Function} timeoutCallback - A callback that is fired on expiration (post removal) + * @returns {Object} The MemoryCache instance + */ function put (key, value, ttl, timeoutCallback) { if (this.debug) { console.log('caching: %s = %j (@%s)', key, value, ttl); @@ -50,6 +67,12 @@ function put (key, value, ttl, timeoutCallback) { return value; }; +/** + * Deletes a key/value pair from the cache + * @method del + * @param {String} key - A unique key + * @returns {Boolean} True if a record was removed from the cache (a hit) or false if the record was not found (a miss) + */ function del (key) { if (this.debug) { console.log('Deleting key ', key); @@ -67,6 +90,10 @@ function del (key) { return false; }; +/** + * Resets the cache to it's original state + * @method clear + */ function clear () { for (var entry of this.cache) { clearTimeout(entry[1].timeout); @@ -76,6 +103,11 @@ function clear () { this.missCount = 0; }; +/** + * Disables a timer (timeout/expiration) for a specifiy key/value pair + * @method killTimer + * @param {String} key - A unique key + */ function killTimer(key) { var obj = this.cache.get(key); if (obj && obj.timeout) { @@ -83,6 +115,12 @@ function killTimer(key) { } }; +/** + * Retrieves a value given a key from the cache + * @method get + * @param {String} key - A unique key + * @returns {Any|undefined} Returns the value for the key in the cache or undefined if not found + */ function get (key) { var data = this.cache.get(key); if (typeof data != "undefined") { @@ -100,30 +138,61 @@ function get (key) { return undefined; }; +/** + * @method size + * @returns {Number} The number of key/value pairs in the cache + */ function size () { return this.cache.size; }; +/** + * Toggles debug statements + * @method setDebug + * @param {Boolean} bool - The value to set debug + */ function setDebug (bool) { this.debug = bool; }; +/** + * @method hits + * @returns {Number} The number of values successfully retrieved via get() + */ function hits () { return this.hitCount; }; +/** + * @method misses + * @returns {Number} The number of unsuccessfully get attempts + */ function misses () { return this.missCount; }; +/** + * @method keys + * @returns {Array} An array of all the keys in the map + */ function keys () { return Array.from(this.cache.keys()); }; +/** + * @method toArray + * @returns {Array} An array of all the values in the map + */ function toArray() { return Array.from(this.cache.values()); } +/** + * @method map + * @param {Function} functor - A function that transforms a value for a given key/value pair + * @param {Object} context - The context for the functor call + * @returns {Map} A map containing key/value pairs where the original value was transformed by the provided functor + */ function map(functor, context) { context = context || this; var result = new Map(); @@ -137,6 +206,12 @@ function map(functor, context) { return result; } +/** + * @method filter + * @param {Function} predicate - A filter function + * @param {Object} context - The context for the predicate call + * @returns {Map} A map containing truthy results of a provided filter function + */ function filter(predicate, context) { context = context || this; var result = new Map(); diff --git a/interfaces/ServiceProvider.js b/interfaces/ServiceProvider.js index 808ddd9e30..4f2377ffce 100644 --- a/interfaces/ServiceProvider.js +++ b/interfaces/ServiceProvider.js @@ -1,12 +1,36 @@ -function ServiceProviderInterface() { +/** + * Interface for service providers + * + * @interface + */ +function ServiceProvider() { }; -ServiceProviderInterface.prototype.getAdapter = function() { +/** + * Get the adapter + * + * @returns {Object} An adapter instance + */ +ServiceProvider.prototype.getAdapter = function() { throw new Error('A service provider must implement getAdapter!'); } -ServiceProviderInterface.prototype.setAdapter = function() { +/** + * Set the adapter + * + * @param {Object} An adapter + */ +ServiceProvider.prototype.setAdapter = function() { throw new Error('A service provider must implement setAdapter!'); } -exports = module.exports = ServiceProviderInterface; \ No newline at end of file +/** + * Resolves the adapter from the first parameter + * + * @param {Any} + */ +ServiceProvider.prototype.resolveAdapter = function() { + throw new Error('A service provider must implement resolveAdapter!'); +} + +exports = module.exports = ServiceProvider; \ No newline at end of file diff --git a/src/Auth.js b/src/Auth.js index 04c0fde0ef..00894febe1 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -1,6 +1,7 @@ var deepcopy = require('deepcopy'); var Parse = require('parse/node').Parse; var RestQuery = require('./RestQuery'); +var CacheProvider = require('./classes/CacheProvider'); // An Auth object tells you who is requesting something and whether // the master key was used. @@ -41,7 +42,7 @@ function nobody(config) { // Returns a promise that resolves to an Auth object var getAuthForSessionToken = function(config, sessionToken) { - var cache = require('./classes/CacheProvider').getAdapter(); + var cache = CacheProvider.getAdapter(); var cachedUser = cache.get(sessionToken); if (cachedUser) { diff --git a/src/index.js b/src/index.js index df63eebb0e..c3127cb206 100644 --- a/src/index.js +++ b/src/index.js @@ -41,16 +41,13 @@ addParseCloud(); // "restAPIKey": optional key from Parse dashboard // "javascriptKey": optional key from Parse dashboard -var DefaultCacheAdapter = require('./classes/MemoryCache'); - function ParseServer(args) { if (!args.appId || !args.masterKey) { throw 'You must provide an appId and masterKey!'; } - this.setupCache(args.cache); - - var cache = this.cacheProvider.getAdapter(); + // Setup providers + CacheProvider.setup(args.cache); if (args.databaseAdapter) { DatabaseAdapter.setAdapter(args.databaseAdapter); @@ -70,7 +67,7 @@ function ParseServer(args) { } else if (typeof args.cloud === 'string') { require(args.cloud); } else { - throw "argument 'cloud' must either be a string or a function"; + throw new Error("argument 'cloud' must either be a string or a function"); } } @@ -91,11 +88,14 @@ function ParseServer(args) { appInfo['facebookAppIds'].push(process.env.FACEBOOK_APP_ID); } + // Cache the application information indefinitely + var cache = CacheProvider.getAdapter(); cache.put(args.appId, appInfo, Infinity); // Initialize the node client SDK automatically Parse.initialize(args.appId, args.javascriptKey || '', args.masterKey); - if(args.serverURL) { + + if (args.serverURL) { Parse.serverURL = args.serverURL; } @@ -179,33 +179,6 @@ function getClassName(parseClass) { return parseClass; } -function resolveAdapter(adapter, options) { - // Support passing in adapter paths - if (typeof adapter === 'string') { - adapter = require(adapter); - } - - // Instantiate the adapter if the class got passed instead of an instance - if (typeof adapter === 'function') { - adapter = new adapter(options); - } - - return adapter; -} - -// TODO: Add configurable TTLs for cache entries -function setupCache(config) { - config = config || {}; - config.adapter = config.adapter || DefaultCacheAdapter; - - var adapter = this.resolveAdapter(config.adapter, config.options); - CacheProvider.setAdapter(adapter); - this.cacheProvider = CacheProvider; -} - -ParseServer.prototype.setupCache = setupCache; -ParseServer.prototype.resolveAdapter = resolveAdapter; - module.exports = { ParseServer: ParseServer, S3Adapter: S3Adapter diff --git a/src/middlewares.js b/src/middlewares.js index 882c029e77..668850f842 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -1,5 +1,5 @@ var Parse = require('parse/node').Parse; - +var CacheProvider = require('./classes/CacheProvider'); var auth = require('./Auth'); var Config = require('./Config'); @@ -10,7 +10,7 @@ var Config = require('./Config'); // req.config - the Config for this app // req.auth - the Auth for this request function handleParseHeaders(req, res, next) { - var cache = require('./classes/CacheProvider').getAdapter(); + var cache = CacheProvider.getAdapter(); var mountPathLength = req.originalUrl.length - req.url.length; var mountPath = req.originalUrl.slice(0, mountPathLength); var mount = req.protocol + '://' + req.get('host') + mountPath; diff --git a/src/rest.js b/src/rest.js index a9f45deaa0..238a86c283 100644 --- a/src/rest.js +++ b/src/rest.js @@ -11,6 +11,7 @@ var Parse = require('parse/node').Parse; var RestQuery = require('./RestQuery'); var RestWrite = require('./RestWrite'); var triggers = require('./triggers'); +var CacheProvider = require('./classes/CacheProvider'); // Returns a promise for an object with optional keys 'results' and 'count'. function find(config, auth, className, restWhere, restOptions) { @@ -35,7 +36,7 @@ function del(config, auth, className, objectId) { enforceRoleSecurity('delete', className, auth); var inflatedObject; - var cache = require('./classes/CacheProvider').getAdapter(); + var cache = CacheProvider.getAdapter(); return Promise.resolve().then(() => { if (triggers.getTrigger(className, 'beforeDelete') || diff --git a/src/testing-routes.js b/src/testing-routes.js index d965ca2f45..f8c7db4ac5 100644 --- a/src/testing-routes.js +++ b/src/testing-routes.js @@ -4,11 +4,12 @@ var express = require('express'), middlewares = require('./middlewares'), rack = require('hat').rack(); +var CacheProvider = require('./classes/CacheProvider'); var router = express.Router(); // creates a unique app in the cache, with a collection prefix function createApp(req, res) { - var cache = require('./classes/CacheProvider').getAdapter(); + var cache = CacheProvider.getAdapter(); var appId = rack(); cache.put(appId, { 'collectionPrefix': appId + '_', From ac6b6df43858fba042a17d9e6e458cfa77a54e5f Mon Sep 17 00:00:00 2001 From: Alexander Mays Date: Tue, 9 Feb 2016 01:19:42 -0500 Subject: [PATCH 06/14] Moved the requirement statement to the top of the file Signed-off-by: Alexander Mays --- src/DatabaseAdapter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/DatabaseAdapter.js b/src/DatabaseAdapter.js index 2b54b756c0..1c99f7ad83 100644 --- a/src/DatabaseAdapter.js +++ b/src/DatabaseAdapter.js @@ -15,7 +15,7 @@ // Default is ExportAdapter, which uses mongo. var ExportAdapter = require('./ExportAdapter'); - +var CacheProvider = require('./classes/CacheProvider'); var adapter = ExportAdapter; var dbConnections = {}; var databaseURI = 'mongodb://localhost:27017/parse'; @@ -34,7 +34,7 @@ function setAppDatabaseURI(appId, uri) { } function getDatabaseConnection(appId) { - var cache = require('./classes/CacheProvider').getAdapter(); + var cache = CacheProvider.getAdapter(); if (dbConnections[appId]) { return dbConnections[appId]; } From 91feeac0ac679026b47491f676ddce10fb566359 Mon Sep 17 00:00:00 2001 From: Alexander Mays Date: Tue, 9 Feb 2016 02:38:24 -0500 Subject: [PATCH 07/14] Merged in the changes to DatabaseProvider and FilesProvider. Updated implementation of the said providers and added a ParseApp class. Needs to be documented. Signed-off-by: Alexander Mays --- classes/BaseProvider.js | 10 +++++ classes/DatabaseProvider.js | 72 ++++++++++++++++++++++++++++++++++ classes/FilesProvider.js | 23 +++++++++++ classes/ParseApp.js | 30 ++++++++++++++ interfaces/ServiceProvider.js | 4 ++ spec/ParseAPI.spec.js | 4 +- spec/ParseInstallation.spec.js | 4 +- spec/RestCreate.spec.js | 4 +- spec/helper.js | 44 ++++++++++++++------- src/Config.js | 4 +- src/files.js | 18 ++++++--- src/index.js | 58 ++++++++------------------- 12 files changed, 206 insertions(+), 69 deletions(-) create mode 100644 classes/DatabaseProvider.js create mode 100644 classes/FilesProvider.js create mode 100644 classes/ParseApp.js diff --git a/classes/BaseProvider.js b/classes/BaseProvider.js index b61096d110..86758e8405 100644 --- a/classes/BaseProvider.js +++ b/classes/BaseProvider.js @@ -55,8 +55,18 @@ function resolveAdapter(adapter, options) { return adapter; } +function setup (config) { + config = config || {}; + config.adapter = config.adapter || DefaultFilesAdapter; + + var adapter = this.resolveAdapter(config.adapter, config.options); + this.setAdapter(adapter); +} + + BaseProvider.prototype.getAdapter = getAdapter; BaseProvider.prototype.setAdapter = setAdapter; BaseProvider.prototype.resolveAdapter = resolveAdapter; +BaseProvider.prototype.setup = setup; exports = module.exports = BaseProvider; \ No newline at end of file diff --git a/classes/DatabaseProvider.js b/classes/DatabaseProvider.js new file mode 100644 index 0000000000..c757fd199f --- /dev/null +++ b/classes/DatabaseProvider.js @@ -0,0 +1,72 @@ +var BaseProvider = require('./BaseProvider'); +var CacheProvider = require('./CacheProvider'); +var util = require('util'); + +var DefaultDatabaseAdapter = require('../ExportAdapter'); +var defaultURI = "mongodb://localhost:27017/parse"; + +function DatabaseProvider(adapter) { + DatabaseProvider.super_.call(this) +}; + +function setup(config) { + config = config || {}; + config.adapter = config.adapter || DefaultDatabaseAdapter; + this.dbConnections = config.dbConnections || this.dbConnections || {}; + this.databaseURI = config.defaultURI || defaultURI; + this.appDatabaseURIs = config.appDatabaseURIs || {}; + + var adapter = this.resolveAdapter(config.adapter, config.options); + this.setAdapter(adapter); +} + +// TODO: Reimplement this whenever @Flovilmart finishes running CloudCode in subprocesses +function registerAppDatabaseURI(appId, uri) { + this.appDatabaseURIs[appId] = uri; +} + +function getDatabaseConnections() { + return this.dbConnections; +} + +function getDatabaseConnection(appId) { + if (this.dbConnections[appId]) { + return this.dbConnections[appId]; + } + + var cache = CacheProvider.getAdapter(); + var app = cache.get(appId); + + if (!app) { + throw new Error('Application ID provided is not a registered application.'); + } + + var adapterFn = this.getAdapter(); + var dbURI = this.appDatabaseURIs[appId] || this.databaseURI; + var options = { collectionPrefix: app.collectionPrefix }; + + this.dbConnections[appId] = new adapterFn(dbURI, options); + this.dbConnections[appId].connect(); + return this.dbConnections[appId]; +} + +// Overriding resolveAdapter to return the class, rather than an instance +function resolveAdapter(adapter, options) { + // Support passing in adapter paths + if (typeof adapter === 'string') { + adapter = require(adapter); + } + + return adapter; +} + +util.inherits(DatabaseProvider, BaseProvider); + +DatabaseProvider.prototype.setup = setup; +DatabaseProvider.prototype.registerAppDatabaseURI = registerAppDatabaseURI; +DatabaseProvider.prototype.getDatabaseConnections = getDatabaseConnections; +DatabaseProvider.prototype.getDatabaseConnection = getDatabaseConnection; +DatabaseProvider.prototype.resolveAdapter = resolveAdapter; +DatabaseProvider.prototype.DatabaseProvider = DatabaseProvider; + +exports = module.exports = new DatabaseProvider(); \ No newline at end of file diff --git a/classes/FilesProvider.js b/classes/FilesProvider.js new file mode 100644 index 0000000000..7571c1e1d4 --- /dev/null +++ b/classes/FilesProvider.js @@ -0,0 +1,23 @@ +var BaseProvider = require('./BaseProvider'); +var util = require('util'); + +var DefaultFilesAdapter = require('../GridStoreAdapter'); + +function FilesProvider(adapter) { + FilesProvider.super_.call(this) +}; + +function setup (config) { + config = config || {}; + config.adapter = config.adapter || DefaultFilesAdapter; + + var adapter = this.resolveAdapter(config.adapter, config.options); + this.setAdapter(adapter); +} + +util.inherits(FilesProvider, BaseProvider); + +FilesProvider.prototype.setup = setup; +FilesProvider.prototype.FilesProvider = FilesProvider; + +exports = module.exports = new FilesProvider(); \ No newline at end of file diff --git a/classes/ParseApp.js b/classes/ParseApp.js new file mode 100644 index 0000000000..ee9c1f16f3 --- /dev/null +++ b/classes/ParseApp.js @@ -0,0 +1,30 @@ +var DatabaseProvider = require('./DatabaseProvider'); + +function ParseApp(args) { + if (!args.appId || !args.masterKey) { + throw 'You must provide an appId and masterKey!'; + } + + this.appId = args.appId; + this.masterKey = args.masterKey; + this.collectionPrefix = args.collectionPrefix || ''; + this.clientKey = args.clientKey || ''; + this.javascriptKey = args.javascriptKey || ''; + this.dotNetKey = args.dotNetKey || ''; + this.restAPIKey = args.restAPIKey || ''; + this.fileKey = args.fileKey || 'invalid-file-key'; + this.facebookAppIds = args.facebookAppIds || []; + this.databaseURI = args.databaseURI; + + // To maintain compatibility. TODO: Remove in v2.1 + if (process.env.FACEBOOK_APP_ID) { + this['facebookAppIds'].push(process.env.FACEBOOK_APP_ID); + } + + // Register with the database provider if we have an app specific database URI + if (this.databaseURI) { + DatabaseProvider.registerAppDatabaseURI(this.appId, this.databaseURI); + } +} + +exports = module.exports = ParseApp; \ No newline at end of file diff --git a/interfaces/ServiceProvider.js b/interfaces/ServiceProvider.js index 4f2377ffce..a6f9b2c64b 100644 --- a/interfaces/ServiceProvider.js +++ b/interfaces/ServiceProvider.js @@ -33,4 +33,8 @@ ServiceProvider.prototype.resolveAdapter = function() { throw new Error('A service provider must implement resolveAdapter!'); } +ServiceProvider.prototype.setup = function() { + throw new Error('A service provider must implement setup!'); +} + exports = module.exports = ServiceProvider; \ No newline at end of file diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 8670bdd2be..57f4911907 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -1,7 +1,7 @@ // A bunch of different tests are in here - it isn't very thematic. // It would probably be better to refactor them into different files. -var DatabaseAdapter = require('../src/DatabaseAdapter'); +var DatabaseProvider = require('../classes/DatabaseProvider'); var request = require('request'); describe('miscellaneous', function() { @@ -358,7 +358,7 @@ describe('miscellaneous', function() { obj.set('foo', 'bar'); return obj.save(); }).then(() => { - var db = DatabaseAdapter.getDatabaseConnection(appId); + var db = DatabaseProvider.getDatabaseConnection(appId); return db.mongoFind('TestObject', {}, {}); }).then((results) => { expect(results.length).toEqual(1); diff --git a/spec/ParseInstallation.spec.js b/spec/ParseInstallation.spec.js index 1c53c98d96..8c9c898345 100644 --- a/spec/ParseInstallation.spec.js +++ b/spec/ParseInstallation.spec.js @@ -3,12 +3,12 @@ var auth = require('../Auth'); var Config = require('../Config'); -var DatabaseAdapter = require('../DatabaseAdapter'); +var DatabaseProvider = require('../classes/DatabaseProvider'); var Parse = require('parse/node').Parse; var rest = require('../src/rest'); var config = new Config('test'); -var database = DatabaseAdapter.getDatabaseConnection('test'); +var database = DatabaseProvider.getDatabaseConnection('test'); describe('Installations', () => { diff --git a/spec/RestCreate.spec.js b/spec/RestCreate.spec.js index b2f5b89b22..21ce55adec 100644 --- a/spec/RestCreate.spec.js +++ b/spec/RestCreate.spec.js @@ -1,13 +1,13 @@ // These tests check the "create" functionality of the REST API. var auth = require('../Auth'); var Config = require('../Config'); -var DatabaseAdapter = require('../DatabaseAdapter'); +var DatabaseProvider = require('../classes/DatabaseProvider'); var Parse = require('parse/node').Parse; var rest = require('../src/rest'); var request = require('request'); var config = new Config('test'); -var database = DatabaseAdapter.getDatabaseConnection('test'); +var database = DatabaseProvider.getDatabaseConnection('test'); describe('rest create', () => { it('handles _id', (done) => { diff --git a/spec/helper.js b/spec/helper.js index 3f762b26a7..bde0be09e5 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -2,7 +2,7 @@ jasmine.DEFAULT_TIMEOUT_INTERVAL = 2000; -var DatabaseAdapter = require('../DatabaseAdapter'); +var DatabaseProvider = require('../classes/DatabaseProvider'); var express = require('express'); var facebook = require('../src/facebook'); var ParseServer = require('../src/index').ParseServer; @@ -10,19 +10,32 @@ var ParseServer = require('../src/index').ParseServer; var databaseURI = process.env.DATABASE_URI; var cloudMain = process.env.CLOUD_CODE_MAIN || './cloud/main.js'; +var config = { + database: { + databaseURI: databaseURI, + /** adapter: "../ExportAdapter" */ + }, + cache: { + }, + files: { + }, + cloud: { + entry: cloudMain + }, + app: { + appId: 'test', + javascriptKey: 'test', + dotNetKey: 'windows', + clientKey: 'client', + restAPIKey: 'rest', + masterKey: 'test', + collectionPrefix: 'test_', + fileKey: 'test' + } +}; + // Set up an API server for testing -var api = new ParseServer({ - databaseURI: databaseURI, - cloud: cloudMain, - appId: 'test', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - collectionPrefix: 'test_', - fileKey: 'test' -}); +var api = new ParseServer(config); var app = express(); app.use('/1', api); @@ -190,8 +203,9 @@ function mockFacebook() { function clearData() { var promises = []; - for (var conn in DatabaseAdapter.dbConnections) { - promises.push(DatabaseAdapter.dbConnections[conn].deleteEverything()); + var connections = DatabaseProvider.getDatabaseConnections(); + for (var conn in connections) { + promises.push(connections[conn].deleteEverything()); } return Promise.all(promises); } diff --git a/src/Config.js b/src/Config.js index e125857dcc..d6e568877a 100644 --- a/src/Config.js +++ b/src/Config.js @@ -3,7 +3,7 @@ // mount is the URL for the root of the API; includes http, domain, etc. function Config(applicationId, mount) { var cache = require('./classes/CacheProvider').getAdapter(); - var DatabaseAdapter = require('./DatabaseAdapter'); + var DatabaseProvider = require('./classes/DatabaseProvider'); var cacheInfo = cache.get(applicationId); this.valid = !!cacheInfo; @@ -13,7 +13,7 @@ function Config(applicationId, mount) { this.applicationId = applicationId; this.collectionPrefix = cacheInfo.collectionPrefix || ''; - this.database = DatabaseAdapter.getDatabaseConnection(applicationId); + this.database = DatabaseProvider.getDatabaseConnection(applicationId); this.masterKey = cacheInfo.masterKey; this.clientKey = cacheInfo.clientKey; this.javascriptKey = cacheInfo.javascriptKey; diff --git a/src/files.js b/src/files.js index 86cdbfbe13..79eac6b408 100644 --- a/src/files.js +++ b/src/files.js @@ -8,11 +8,16 @@ var bodyParser = require('body-parser'), Parse = require('parse/node').Parse, rack = require('hat').rack(); -import { getAdapter as getFilesAdapter } from './FilesAdapter'; - +var FilesProvider = require('./classes/FilesProvider'); var router = express.Router(); var processCreate = function(req, res, next) { + var FilesAdapter = FilesProvider.getAdapter(); + + if (!FilesAdapter) { + throw new Error('Unable to get an instance of the FilesAdapter'); + } + if (!req.body || !req.body.length) { next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'Invalid file upload.')); @@ -41,9 +46,10 @@ var processCreate = function(req, res, next) { } var filename = rack() + '_' + req.params.filename + extension; - getFilesAdapter().createFileAsync(req.config, filename, req.body).then(() => { + FilesAdapter.create(req.config, filename, req.body) + .then(() => { res.status(201); - var location = getFilesAdapter().getFileLocation(req.config, req, filename); + var location = FilesAdapter.location(req.config, req, filename); res.set('Location', location); res.json({ url: location, name: filename }); }).catch((error) => { @@ -54,8 +60,10 @@ var processCreate = function(req, res, next) { }; var processGet = function(req, res) { + var FilesAdapter = FilesProvider.getAdapter(); var config = new Config(req.params.appId); - getFilesAdapter().getFileDataAsync(config, req.params.filename).then((data) => { + FilesAdapter.get(config, req.params.filename) + .then((data) => { res.status(200); var contentType = mime.lookup(req.params.filename); res.set('Content-type', contentType); diff --git a/src/index.js b/src/index.js index c3127cb206..854eb59514 100644 --- a/src/index.js +++ b/src/index.js @@ -2,8 +2,6 @@ var batch = require('./batch'), bodyParser = require('body-parser'), - CacheProvider = require('./classes/CacheProvider'), - DatabaseAdapter = require('./DatabaseAdapter'), express = require('express'), S3Adapter = require('./S3Adapter'), middlewares = require('./middlewares'), @@ -12,8 +10,10 @@ var batch = require('./batch'), PromiseRouter = require('./PromiseRouter'), httpRequest = require('./httpRequest'); -import { setAdapter as setFilesAdapter } from './FilesAdapter'; -import { default as GridStoreAdapter } from './GridStoreAdapter'; +var ParseApp = require('./classes/ParseApp'); +var CacheProvider = require('./classes/CacheProvider'); +var FilesProvider = require('./classes/FilesProvider'); +var DatabaseProvider = require('./classes/DatabaseProvider'); // Mutate the Parse object to add the Cloud Code handlers addParseCloud(); @@ -42,55 +42,31 @@ addParseCloud(); // "javascriptKey": optional key from Parse dashboard function ParseServer(args) { - if (!args.appId || !args.masterKey) { - throw 'You must provide an appId and masterKey!'; - } - // Setup providers CacheProvider.setup(args.cache); + FilesProvider.setup(args.files); + DatabaseProvider.setup(args.database); + + // Instantiate the app + var app = new ParseApp(args.app); - if (args.databaseAdapter) { - DatabaseAdapter.setAdapter(args.databaseAdapter); - } - if (args.filesAdapter) { - setFilesAdapter(args.filesAdapter); - } else { - setFilesAdapter(new GridStoreAdapter()); - } - if (args.databaseURI) { - DatabaseAdapter.setAppDatabaseURI(args.appId, args.databaseURI); - } if (args.cloud) { + // Add the Parse.Cloud global function definitions addParseCloud(); - if (typeof args.cloud === 'function') { - args.cloud(Parse) - } else if (typeof args.cloud === 'string') { - require(args.cloud); + + // Load the cloud code entry point + if (typeof args.cloud.entry === 'function') { + args.cloud.entry(Parse) + } else if (typeof args.cloud.entry === 'string') { + require(args.cloud.entry); } else { throw new Error("argument 'cloud' must either be a string or a function"); } - - } - - var appInfo = { - masterKey: args.masterKey, - collectionPrefix: args.collectionPrefix || '', - clientKey: args.clientKey || '', - javascriptKey: args.javascriptKey || '', - dotNetKey: args.dotNetKey || '', - restAPIKey: args.restAPIKey || '', - fileKey: args.fileKey || 'invalid-file-key', - facebookAppIds: args.facebookAppIds || [] - }; - - // To maintain compatibility. TODO: Remove in v2.1 - if (process.env.FACEBOOK_APP_ID) { - appInfo['facebookAppIds'].push(process.env.FACEBOOK_APP_ID); } // Cache the application information indefinitely var cache = CacheProvider.getAdapter(); - cache.put(args.appId, appInfo, Infinity); + cache.put(app.appId, app, Infinity); // Initialize the node client SDK automatically Parse.initialize(args.appId, args.javascriptKey || '', args.masterKey); From 9488a9e2bbc0473addd14cfcd1d31fea7303e8dd Mon Sep 17 00:00:00 2001 From: Alexander Mays Date: Tue, 9 Feb 2016 03:03:44 -0500 Subject: [PATCH 08/14] Rebased and moved to the new structure Signed-off-by: Alexander Mays --- {classes => src/classes}/BaseProvider.js | 0 {classes => src/classes}/CacheProvider.js | 0 {classes => src/classes}/DatabaseProvider.js | 0 {classes => src/classes}/FilesProvider.js | 0 {classes => src/classes}/MemoryCache.js | 0 {classes => src/classes}/ParseApp.js | 0 {interfaces => src/interfaces}/ServiceProvider.js | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename {classes => src/classes}/BaseProvider.js (100%) rename {classes => src/classes}/CacheProvider.js (100%) rename {classes => src/classes}/DatabaseProvider.js (100%) rename {classes => src/classes}/FilesProvider.js (100%) rename {classes => src/classes}/MemoryCache.js (100%) rename {classes => src/classes}/ParseApp.js (100%) rename {interfaces => src/interfaces}/ServiceProvider.js (100%) diff --git a/classes/BaseProvider.js b/src/classes/BaseProvider.js similarity index 100% rename from classes/BaseProvider.js rename to src/classes/BaseProvider.js diff --git a/classes/CacheProvider.js b/src/classes/CacheProvider.js similarity index 100% rename from classes/CacheProvider.js rename to src/classes/CacheProvider.js diff --git a/classes/DatabaseProvider.js b/src/classes/DatabaseProvider.js similarity index 100% rename from classes/DatabaseProvider.js rename to src/classes/DatabaseProvider.js diff --git a/classes/FilesProvider.js b/src/classes/FilesProvider.js similarity index 100% rename from classes/FilesProvider.js rename to src/classes/FilesProvider.js diff --git a/classes/MemoryCache.js b/src/classes/MemoryCache.js similarity index 100% rename from classes/MemoryCache.js rename to src/classes/MemoryCache.js diff --git a/classes/ParseApp.js b/src/classes/ParseApp.js similarity index 100% rename from classes/ParseApp.js rename to src/classes/ParseApp.js diff --git a/interfaces/ServiceProvider.js b/src/interfaces/ServiceProvider.js similarity index 100% rename from interfaces/ServiceProvider.js rename to src/interfaces/ServiceProvider.js From 8d8a01cd6e0e4f45a844b11af4cbd19664272703 Mon Sep 17 00:00:00 2001 From: Alexander Mays Date: Tue, 9 Feb 2016 03:30:57 -0500 Subject: [PATCH 09/14] Fixed the pathing issues with the new structure Signed-off-by: Alexander Mays --- spec/ParseAPI.spec.js | 2 +- spec/ParseInstallation.spec.js | 6 +++--- spec/RestCreate.spec.js | 6 +++--- spec/RestQuery.spec.js | 6 +++--- spec/classes/MemoryCache.spec.js | 2 +- spec/helper.js | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 57f4911907..372ff85c85 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -1,7 +1,7 @@ // A bunch of different tests are in here - it isn't very thematic. // It would probably be better to refactor them into different files. -var DatabaseProvider = require('../classes/DatabaseProvider'); +var DatabaseProvider = require('../src/classes/DatabaseProvider'); var request = require('request'); describe('miscellaneous', function() { diff --git a/spec/ParseInstallation.spec.js b/spec/ParseInstallation.spec.js index 8c9c898345..5bd10f3370 100644 --- a/spec/ParseInstallation.spec.js +++ b/spec/ParseInstallation.spec.js @@ -1,9 +1,9 @@ // These tests check the Installations functionality of the REST API. // Ported from installation_collection_test.go -var auth = require('../Auth'); -var Config = require('../Config'); -var DatabaseProvider = require('../classes/DatabaseProvider'); +var auth = require('../src/Auth'); +var Config = require('../src/Config'); +var DatabaseProvider = require('../src/classes/DatabaseProvider'); var Parse = require('parse/node').Parse; var rest = require('../src/rest'); diff --git a/spec/RestCreate.spec.js b/spec/RestCreate.spec.js index 21ce55adec..5d92a651f7 100644 --- a/spec/RestCreate.spec.js +++ b/spec/RestCreate.spec.js @@ -1,7 +1,7 @@ // These tests check the "create" functionality of the REST API. -var auth = require('../Auth'); -var Config = require('../Config'); -var DatabaseProvider = require('../classes/DatabaseProvider'); +var auth = require('../src/Auth'); +var Config = require('../src/Config'); +var DatabaseProvider = require('../src/classes/DatabaseProvider'); var Parse = require('parse/node').Parse; var rest = require('../src/rest'); var request = require('request'); diff --git a/spec/RestQuery.spec.js b/spec/RestQuery.spec.js index e2aca8d90a..9ca5784b91 100644 --- a/spec/RestQuery.spec.js +++ b/spec/RestQuery.spec.js @@ -1,7 +1,7 @@ // These tests check the "find" functionality of the REST API. -var auth = require('../Auth'); -var Config = require('../Config'); -var rest = require('../rest'); +var auth = require('../src/Auth'); +var Config = require('../src/Config'); +var rest = require('../src/rest'); var config = new Config('test'); var nobody = auth.nobody(config); diff --git a/spec/classes/MemoryCache.spec.js b/spec/classes/MemoryCache.spec.js index 87e9e70a03..d53deaf246 100644 --- a/spec/classes/MemoryCache.spec.js +++ b/spec/classes/MemoryCache.spec.js @@ -1,7 +1,7 @@ /* global describe, it, before, beforeEach, afterEach */ 'use strict'; -var cache = new (require('../../classes/MemoryCache')); +var cache = new (require('../../src/classes/MemoryCache')); var _ = require('lodash'); describe('MemoryCache', function() { diff --git a/spec/helper.js b/spec/helper.js index bde0be09e5..77cc3a7377 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -2,7 +2,7 @@ jasmine.DEFAULT_TIMEOUT_INTERVAL = 2000; -var DatabaseProvider = require('../classes/DatabaseProvider'); +var DatabaseProvider = require('../src/classes/DatabaseProvider'); var express = require('express'); var facebook = require('../src/facebook'); var ParseServer = require('../src/index').ParseServer; From 6b29cbcad68e05a4d6172d922b9956d989c9e1dd Mon Sep 17 00:00:00 2001 From: Alexander Mays Date: Tue, 9 Feb 2016 04:54:53 -0500 Subject: [PATCH 10/14] Updated to ES6 Signed-off-by: Alexander Mays --- spec/ParseAPI.spec.js | 2 +- spec/ParseInstallation.spec.js | 2 +- spec/RestCreate.spec.js | 2 +- spec/classes/MemoryCache.spec.js | 4 +- spec/helper.js | 2 +- src/Auth.js | 2 +- src/Config.js | 6 +- src/classes/BaseProvider.js | 98 ++++---- src/classes/CacheProvider.js | 35 +-- src/classes/DatabaseProvider.js | 99 ++++---- src/classes/FilesProvider.js | 27 +- src/classes/MemoryCache.js | 400 +++++++++++++++--------------- src/classes/ParseApp.js | 30 ++- src/classes/index.js | 6 + src/files.js | 2 +- src/index.js | 8 +- src/interfaces/ServiceProvider.js | 55 ++-- src/middlewares.js | 2 +- src/rest.js | 2 +- src/testing-routes.js | 2 +- 20 files changed, 371 insertions(+), 415 deletions(-) create mode 100644 src/classes/index.js diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 372ff85c85..4f70711bfa 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -1,7 +1,7 @@ // A bunch of different tests are in here - it isn't very thematic. // It would probably be better to refactor them into different files. -var DatabaseProvider = require('../src/classes/DatabaseProvider'); +var DatabaseProvider = require('../src/classes/DatabaseProvider').default; var request = require('request'); describe('miscellaneous', function() { diff --git a/spec/ParseInstallation.spec.js b/spec/ParseInstallation.spec.js index 5bd10f3370..08d8775f15 100644 --- a/spec/ParseInstallation.spec.js +++ b/spec/ParseInstallation.spec.js @@ -3,7 +3,7 @@ var auth = require('../src/Auth'); var Config = require('../src/Config'); -var DatabaseProvider = require('../src/classes/DatabaseProvider'); +var DatabaseProvider = require('../src/classes/DatabaseProvider').default; var Parse = require('parse/node').Parse; var rest = require('../src/rest'); diff --git a/spec/RestCreate.spec.js b/spec/RestCreate.spec.js index 5d92a651f7..04d3595102 100644 --- a/spec/RestCreate.spec.js +++ b/spec/RestCreate.spec.js @@ -1,7 +1,7 @@ // These tests check the "create" functionality of the REST API. var auth = require('../src/Auth'); var Config = require('../src/Config'); -var DatabaseProvider = require('../src/classes/DatabaseProvider'); +var DatabaseProvider = require('../src/classes/DatabaseProvider').default; var Parse = require('parse/node').Parse; var rest = require('../src/rest'); var request = require('request'); diff --git a/spec/classes/MemoryCache.spec.js b/spec/classes/MemoryCache.spec.js index d53deaf246..daf7cd046b 100644 --- a/spec/classes/MemoryCache.spec.js +++ b/spec/classes/MemoryCache.spec.js @@ -1,7 +1,9 @@ /* global describe, it, before, beforeEach, afterEach */ 'use strict'; -var cache = new (require('../../src/classes/MemoryCache')); +var MemoryCache = require('../../src/classes/MemoryCache').default; + +var cache = new MemoryCache(); var _ = require('lodash'); describe('MemoryCache', function() { diff --git a/spec/helper.js b/spec/helper.js index 77cc3a7377..ab42b4a310 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -2,7 +2,7 @@ jasmine.DEFAULT_TIMEOUT_INTERVAL = 2000; -var DatabaseProvider = require('../src/classes/DatabaseProvider'); +var DatabaseProvider = require('../src/classes/DatabaseProvider').default; var express = require('express'); var facebook = require('../src/facebook'); var ParseServer = require('../src/index').ParseServer; diff --git a/src/Auth.js b/src/Auth.js index 00894febe1..ef8809e82f 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -1,7 +1,7 @@ var deepcopy = require('deepcopy'); var Parse = require('parse/node').Parse; var RestQuery = require('./RestQuery'); -var CacheProvider = require('./classes/CacheProvider'); +var CacheProvider = require('./classes/CacheProvider').default; // An Auth object tells you who is requesting something and whether // the master key was used. diff --git a/src/Config.js b/src/Config.js index d6e568877a..f24f8340b7 100644 --- a/src/Config.js +++ b/src/Config.js @@ -1,9 +1,11 @@ // A Config object provides information about how a specific app is // configured. // mount is the URL for the root of the API; includes http, domain, etc. + function Config(applicationId, mount) { - var cache = require('./classes/CacheProvider').getAdapter(); - var DatabaseProvider = require('./classes/DatabaseProvider'); + var DatabaseProvider = require('./classes/DatabaseProvider').default; + var CacheProvider = require('./classes/CacheProvider').default; + var cache = CacheProvider.getAdapter(); var cacheInfo = cache.get(applicationId); this.valid = !!cacheInfo; diff --git a/src/classes/BaseProvider.js b/src/classes/BaseProvider.js index 86758e8405..10e3294a89 100644 --- a/src/classes/BaseProvider.js +++ b/src/classes/BaseProvider.js @@ -1,6 +1,4 @@ -var ServiceProviderInterface = require('../interfaces/ServiceProvider'); -var util = require('util'); - +import { default as ServiceProviderInterface } from '../interfaces/ServiceProvider'; /** * A base provider class that allows for an abstraction of adapter implementations * @@ -8,65 +6,61 @@ var util = require('util'); * @implements {ServiceProvider} * @param {Object} adapter - An adapter */ -function BaseProvider(adapter) { +export class BaseProvider { + constructor(adapter){ if (adapter) { this.adapter = adapter; } -}; + } -util.inherits(BaseProvider, ServiceProviderInterface); + /** + * Get the adapter + * + * @returns {Object} An adapter instance + */ + getAdapter() { + return this.adapter; + } -/** - * Get the adapter - * - * @returns {Object} An adapter instance - */ -function getAdapter() { - return this.adapter; -} - -/** - * Set the adapter - * - * @param {Object} adapter - An adapter - */ -function setAdapter(adapter) { - this.adapter = adapter; -} + /** + * Set the adapter + * + * @param {Object} adapter - An adapter + */ + setAdapter(adapter) { + this.adapter = adapter; + } -/** - * Resolves the adapter - * - * @param {Object|String|Function} adapter - [1] An object implementing the adapter interface, or [2] a function that returns [1], or [3] A string of either the name of an included npm module or a path to a local module that returns [1] or [2]. - * @param {Object} options - An object passed to the adapter on instantiation (if adapter is not already instantiated) - * @returns {Object} An object implementing the adapter interface - */ -function resolveAdapter(adapter, options) { - // Support passing in adapter paths - if (typeof adapter === 'string') { - adapter = require(adapter); - } + /** + * Resolves the adapter + * + * @param {Object|String|Function} adapter - [1] An object implementing the adapter interface, or [2] a function that returns [1], or [3] A string of either the name of an included npm module or a path to a local module that returns [1] or [2]. + * @param {Object} options - An object passed to the adapter on instantiation (if adapter is not already instantiated) + * @returns {Object} An object implementing the adapter interface + */ + resolveAdapter(adapter, options) { + // Support passing in adapter paths + if (typeof adapter === 'string') { + adapter = require(adapter); - // Instantiate the adapter if the class got passed instead of an instance - if (typeof adapter === 'function') { - adapter = new adapter(options); - } + // TODO: Figure out a better way to deal with this + if (adapter && adapter.default) + adapter = adapter.default; + } - return adapter; -} + // Instantiate the adapter if the class got passed instead of an instance + if (typeof adapter === 'function') { + adapter = new adapter(options); + } -function setup (config) { - config = config || {}; - config.adapter = config.adapter || DefaultFilesAdapter; + return adapter; + } - var adapter = this.resolveAdapter(config.adapter, config.options); - this.setAdapter(adapter); + setup (config = {}) { + const adapter = this.resolveAdapter(config.adapter || this.DEFAULT_ADAPTER, config.options); + this.setAdapter(adapter); + } } -BaseProvider.prototype.getAdapter = getAdapter; -BaseProvider.prototype.setAdapter = setAdapter; -BaseProvider.prototype.resolveAdapter = resolveAdapter; -BaseProvider.prototype.setup = setup; - -exports = module.exports = BaseProvider; \ No newline at end of file +export default BaseProvider; \ No newline at end of file diff --git a/src/classes/CacheProvider.js b/src/classes/CacheProvider.js index 7618e74941..5ecdbf6736 100644 --- a/src/classes/CacheProvider.js +++ b/src/classes/CacheProvider.js @@ -1,7 +1,4 @@ -var util = require('util'); - -var BaseProvider = require('./BaseProvider'); -var DefaultCacheAdapter = require('./MemoryCache'); +import { default as BaseProvider } from './BaseProvider'; /** * Abstract class the provides a reference to an adapter instance (a caching implementation) @@ -10,29 +7,11 @@ var DefaultCacheAdapter = require('./MemoryCache'); * @extends {BaseProvider} * @param {Object} adapter - A cache adapter */ -function CacheProvider(adapter) { - CacheProvider.super_.call(this) -}; - -/** -* Setup the cache provider given a configuration object -* -* @method -* @param {Object} config - A configuration object -* @param {Any} config.adapter - A string, object, instance, or function that resolves to an adapter implementation -* @param {Object} config.options - An object passed to the adapter on instantiation (if adapter is not already instantiated) -*/ -function setup (config) { - config = config || {}; - config.adapter = config.adapter || DefaultCacheAdapter; - - var adapter = this.resolveAdapter(config.adapter, config.options); - this.setAdapter(adapter); +export class CacheProvider extends BaseProvider { + constructor() { + super(...arguments); + this.DEFAULT_ADAPTER = './MemoryCache'; + } } -util.inherits(CacheProvider, BaseProvider); - -CacheProvider.prototype.setup = setup; -CacheProvider.prototype.CacheProvider = CacheProvider; - -exports = module.exports = new CacheProvider(); \ No newline at end of file +export default new CacheProvider(); \ No newline at end of file diff --git a/src/classes/DatabaseProvider.js b/src/classes/DatabaseProvider.js index c757fd199f..fcac1dd246 100644 --- a/src/classes/DatabaseProvider.js +++ b/src/classes/DatabaseProvider.js @@ -1,72 +1,65 @@ -var BaseProvider = require('./BaseProvider'); -var CacheProvider = require('./CacheProvider'); -var util = require('util'); +import { default as BaseProvider } from './BaseProvider'; +import { default as CacheProvider } from './CacheProvider'; -var DefaultDatabaseAdapter = require('../ExportAdapter'); -var defaultURI = "mongodb://localhost:27017/parse"; +const DEFAULT_URI = "mongodb://localhost:27017/parse"; -function DatabaseProvider(adapter) { - DatabaseProvider.super_.call(this) -}; +export class DatabaseProvider extends BaseProvider { + constructor() { + super(...arguments); + this.DEFAULT_ADAPTER = '../ExportAdapter'; + } -function setup(config) { - config = config || {}; - config.adapter = config.adapter || DefaultDatabaseAdapter; + setup(config = {}) { this.dbConnections = config.dbConnections || this.dbConnections || {}; - this.databaseURI = config.defaultURI || defaultURI; + this.databaseURI = config.defaultURI || DEFAULT_URI; this.appDatabaseURIs = config.appDatabaseURIs || {}; - var adapter = this.resolveAdapter(config.adapter, config.options); - this.setAdapter(adapter); -} + super.setup(...arguments); + } -// TODO: Reimplement this whenever @Flovilmart finishes running CloudCode in subprocesses -function registerAppDatabaseURI(appId, uri) { - this.appDatabaseURIs[appId] = uri; -} + // TODO: Reimplement this whenever @Flovilmart finishes running CloudCode in subprocesses + registerAppDatabaseURI(appId, uri) { + this.appDatabaseURIs[appId] = uri; + } -function getDatabaseConnections() { + getDatabaseConnections() { return this.dbConnections; -} - -function getDatabaseConnection(appId) { - if (this.dbConnections[appId]) { - return this.dbConnections[appId]; } - var cache = CacheProvider.getAdapter(); - var app = cache.get(appId); + getDatabaseConnection(appId) { + if (this.dbConnections[appId]) { + return this.dbConnections[appId]; + } - if (!app) { - throw new Error('Application ID provided is not a registered application.'); - } + const cache = CacheProvider.getAdapter(); + const app = cache.get(appId); - var adapterFn = this.getAdapter(); - var dbURI = this.appDatabaseURIs[appId] || this.databaseURI; - var options = { collectionPrefix: app.collectionPrefix }; + if (!app) { + throw new Error('Application ID provided is not a registered application.'); + } - this.dbConnections[appId] = new adapterFn(dbURI, options); - this.dbConnections[appId].connect(); - return this.dbConnections[appId]; -} + const adapterClass = this.getAdapter(); + const dbURI = this.appDatabaseURIs[appId] || this.databaseURI; + const options = { collectionPrefix: app.collectionPrefix }; -// Overriding resolveAdapter to return the class, rather than an instance -function resolveAdapter(adapter, options) { - // Support passing in adapter paths - if (typeof adapter === 'string') { - adapter = require(adapter); - } + this.dbConnections[appId] = new adapterClass(dbURI, options); + this.dbConnections[appId].connect(); + return this.dbConnections[appId]; + } - return adapter; -} + // Overriding resolveAdapter to prevent instantiation + resolveAdapter(adapter, options) { + // Support passing in adapter paths + if (typeof adapter === 'string') { + adapter = require(adapter); -util.inherits(DatabaseProvider, BaseProvider); + // TODO: Figure out a better way to deal with this + if (adapter && adapter.default) + adapter = adapter.default; + } -DatabaseProvider.prototype.setup = setup; -DatabaseProvider.prototype.registerAppDatabaseURI = registerAppDatabaseURI; -DatabaseProvider.prototype.getDatabaseConnections = getDatabaseConnections; -DatabaseProvider.prototype.getDatabaseConnection = getDatabaseConnection; -DatabaseProvider.prototype.resolveAdapter = resolveAdapter; -DatabaseProvider.prototype.DatabaseProvider = DatabaseProvider; + return adapter; + } +} -exports = module.exports = new DatabaseProvider(); \ No newline at end of file +export default new DatabaseProvider(); \ No newline at end of file diff --git a/src/classes/FilesProvider.js b/src/classes/FilesProvider.js index 7571c1e1d4..8e3b996c53 100644 --- a/src/classes/FilesProvider.js +++ b/src/classes/FilesProvider.js @@ -1,23 +1,10 @@ -var BaseProvider = require('./BaseProvider'); -var util = require('util'); +import { default as BaseProvider } from './BaseProvider'; -var DefaultFilesAdapter = require('../GridStoreAdapter'); - -function FilesProvider(adapter) { - FilesProvider.super_.call(this) -}; - -function setup (config) { - config = config || {}; - config.adapter = config.adapter || DefaultFilesAdapter; - - var adapter = this.resolveAdapter(config.adapter, config.options); - this.setAdapter(adapter); +export class FilesProvider extends BaseProvider { + constructor() { + super(...arguments); + this.DEFAULT_ADAPTER = '../GridStoreAdapter' + } } -util.inherits(FilesProvider, BaseProvider); - -FilesProvider.prototype.setup = setup; -FilesProvider.prototype.FilesProvider = FilesProvider; - -exports = module.exports = new FilesProvider(); \ No newline at end of file +export default new FilesProvider(); \ No newline at end of file diff --git a/src/classes/MemoryCache.js b/src/classes/MemoryCache.js index f2ae43f3dd..076a6d6081 100644 --- a/src/classes/MemoryCache.js +++ b/src/classes/MemoryCache.js @@ -1,14 +1,15 @@ -'use strict'; -// Modified from https://github.com/ptarjan/node-cache/blob/master/index.js - /** -* Creates a new in-memory cache using Map for storage +* In-memory cache using Map for storage * * @class -* @param {Object} options - An object of default options -* @param {String} [options.defaultTtl=600000] - The number of milliseconds to use as the default time-to-live of a cache entry */ -function MemoryCache(options) { +export class MemoryCache { + /** + * @constructor + * @param {Object} options - An object of default options + * @param {String} [options.defaultTtl=600000] - The number of milliseconds to use as the default time-to-live of a cache entry + */ + constructor(options) { options = options || {}; this.cache = new Map(); @@ -16,230 +17,221 @@ function MemoryCache(options) { this.hitCount = 0; this.missCount = 0; this.defaultTtl = options.defaultTtl || 10 * 60 * 1000; -}; - -/** - * Puts a key value mapping into the map that will automatically expire given a TTL. - * @method put - * @param {String} key - A unique key - * @param {Any} value - A value to be stored - * @param {Number} ttl - The number of milliseconds until the key/value pair is removed from the cache - * @param {Function} timeoutCallback - A callback that is fired on expiration (post removal) - * @returns {Object} The MemoryCache instance - */ -function put (key, value, ttl, timeoutCallback) { - if (this.debug) { - console.log('caching: %s = %j (@%s)', key, value, ttl); } - if (typeof ttl !== 'undefined' && (typeof ttl !== 'number' || isNaN(ttl) || ttl <= 0)) { - throw new Error('Cache timeout must be a positive number'); - } else if (typeof timeoutCallback !== 'undefined' && typeof timeoutCallback !== 'function') { - throw new Error('Cache timeout callback must be a function'); - } + /** + * Puts a key value mapping into the map that will automatically expire given a TTL. + * @method put + * @param {String} key - A unique key + * @param {Any} value - A value to be stored + * @param {Number} ttl - The number of milliseconds until the key/value pair is removed from the cache + * @param {Function} timeoutCallback - A callback that is fired on expiration (post removal) + * @returns {Object} The MemoryCache instance + */ + put (key, value, ttl, timeoutCallback) { + if (this.debug) { + console.log('caching: %s = %j (@%s)', key, value, ttl); + } - // TTL can still be set to Infinity for never expiring records - if (ttl === undefined) { - ttl = this.defaultTtl; - } + if (typeof ttl !== 'undefined' && (typeof ttl !== 'number' || isNaN(ttl) || ttl <= 0)) { + throw new Error('Cache timeout must be a positive number'); + } else if (typeof timeoutCallback !== 'undefined' && typeof timeoutCallback !== 'function') { + throw new Error('Cache timeout callback must be a function'); + } - var oldRecord = this.cache.get(key); - if (oldRecord) { - clearTimeout(oldRecord.timeout); - } + // TTL can still be set to Infinity for never expiring records + if (ttl === undefined) { + ttl = this.defaultTtl; + } - var record = { - value: value, - expire: (ttl + Date.now()) - }; + var oldRecord = this.cache.get(key); + if (oldRecord) { + clearTimeout(oldRecord.timeout); + } - if (!isNaN(record.expire) && ttl !== Infinity) { - record.timeout = setTimeout(() => { - this.del(key); - if (timeoutCallback) { - timeoutCallback(key); - } - }, ttl); - } + var record = { + value: value, + expire: (ttl + Date.now()) + }; - this.cache.set(key, record); + if (!isNaN(record.expire) && ttl !== Infinity) { + record.timeout = setTimeout(() => { + this.del(key); + if (timeoutCallback) { + timeoutCallback(key); + } + }, ttl); + } - return value; -}; + this.cache.set(key, record); -/** - * Deletes a key/value pair from the cache - * @method del - * @param {String} key - A unique key - * @returns {Boolean} True if a record was removed from the cache (a hit) or false if the record was not found (a miss) - */ -function del (key) { - if (this.debug) { - console.log('Deleting key ', key); + return value; } - var oldRecord = this.cache.get(key); - if (oldRecord) { - if (oldRecord.timeout) { - clearTimeout(oldRecord.timeout); - } - this.cache.delete(key); - return true; - } - return false; -}; -/** - * Resets the cache to it's original state - * @method clear - */ -function clear () { - for (var entry of this.cache) { - clearTimeout(entry[1].timeout); + /** + * Deletes a key/value pair from the cache + * @method del + * @param {String} key - A unique key + * @returns {Boolean} True if a record was removed from the cache (a hit) or false if the record was not found (a miss) + */ + del (key) { + if (this.debug) { + console.log('Deleting key ', key); + } + var oldRecord = this.cache.get(key); + if (oldRecord) { + if (oldRecord.timeout) { + clearTimeout(oldRecord.timeout); + } + + this.cache.delete(key); + return true; + } + + return false; } - this.cache = new Map(); - this.hitCount = 0; - this.missCount = 0; -}; -/** - * Disables a timer (timeout/expiration) for a specifiy key/value pair - * @method killTimer - * @param {String} key - A unique key - */ -function killTimer(key) { - var obj = this.cache.get(key); - if (obj && obj.timeout) { - clearTimeout(obj.timeout); + /** + * Resets the cache to it's original state + * @method clear + */ + clear () { + for (var entry of this.cache) { + clearTimeout(entry[1].timeout); } -}; + this.cache = new Map(); + this.hitCount = 0; + this.missCount = 0; + }; -/** - * Retrieves a value given a key from the cache - * @method get - * @param {String} key - A unique key - * @returns {Any|undefined} Returns the value for the key in the cache or undefined if not found - */ -function get (key) { - var data = this.cache.get(key); - if (typeof data != "undefined") { - if (isNaN(data.expire) || data.expire >= Date.now()) { - this.hitCount++; - return data.value; + /** + * Disables a timer (timeout/expiration) for a specifiy key/value pair + * @method killTimer + * @param {String} key - A unique key + */ + killTimer(key) { + var obj = this.cache.get(key); + if (obj && obj.timeout) { + clearTimeout(obj.timeout); + } + }; + + /** + * Retrieves a value given a key from the cache + * @method get + * @param {String} key - A unique key + * @returns {Any|undefined} Returns the value for the key in the cache or undefined if not found + */ + get (key) { + var data = this.cache.get(key); + if (typeof data != "undefined") { + if (isNaN(data.expire) || data.expire >= Date.now()) { + this.hitCount++; + return data.value; + } else { + // free some space + this.missCount++; + this.del(key) + } } else { - // free some space this.missCount++; - this.del(key) } - } else { - this.missCount++; - } - return undefined; -}; + return undefined; + }; -/** - * @method size - * @returns {Number} The number of key/value pairs in the cache - */ -function size () { - return this.cache.size; -}; + /** + * @method size + * @returns {Number} The number of key/value pairs in the cache + */ + size () { + return this.cache.size; + }; -/** - * Toggles debug statements - * @method setDebug - * @param {Boolean} bool - The value to set debug - */ -function setDebug (bool) { - this.debug = bool; -}; + /** + * Toggles debug statements + * @method setDebug + * @param {Boolean} bool - The value to set debug + */ + setDebug (bool) { + this.debug = bool; + }; -/** - * @method hits - * @returns {Number} The number of values successfully retrieved via get() - */ -function hits () { - return this.hitCount; -}; + /** + * @method hits + * @returns {Number} The number of values successfully retrieved via get() + */ + hits () { + return this.hitCount; + }; -/** - * @method misses - * @returns {Number} The number of unsuccessfully get attempts - */ -function misses () { - return this.missCount; -}; + /** + * @method misses + * @returns {Number} The number of unsuccessfully get attempts + */ + misses () { + return this.missCount; + }; -/** - * @method keys - * @returns {Array} An array of all the keys in the map - */ -function keys () { - return Array.from(this.cache.keys()); -}; + /** + * @method keys + * @returns {Array} An array of all the keys in the map + */ + keys () { + return Array.from(this.cache.keys()); + }; -/** - * @method toArray - * @returns {Array} An array of all the values in the map - */ -function toArray() { - return Array.from(this.cache.values()); -} + /** + * @method toArray + * @returns {Array} An array of all the values in the map + */ + toArray() { + return Array.from(this.cache.values()); + } -/** - * @method map - * @param {Function} functor - A function that transforms a value for a given key/value pair - * @param {Object} context - The context for the functor call - * @returns {Map} A map containing key/value pairs where the original value was transformed by the provided functor - */ -function map(functor, context) { - context = context || this; - var result = new Map(); - - for (var entry of this.cache.entries()) { - var key = entry[0]; - var value = entry[1]; - result.set(key, functor.call(context, value, key)); - } + /** + * @method map + * @param {Function} functor - A function that transforms a value for a given key/value pair + * @param {Object} context - The context for the functor call + * @returns {Map} A map containing key/value pairs where the original value was transformed by the provided functor + */ + map(functor, context) { + context = context || this; + var result = new Map(); + + for (var entry of this.cache.entries()) { + var key = entry[0]; + var value = entry[1]; + result.set(key, functor.call(context, value, key)); + } + + return result; + } - return result; -} + /** + * @method filter + * @param {Function} predicate - A filter function + * @param {Object} context - The context for the predicate call + * @returns {Map} A map containing truthy results of a provided filter function + */ + filter(predicate, context) { + context = context || this; + var result = new Map(); + + for (var entry of this.cache.entries()) { + var key = entry[0]; + var value = entry[1]; + + if (predicate.call(context, value, key)) { + result.set(key, value); + } + } + + return result; + } + +}; -/** - * @method filter - * @param {Function} predicate - A filter function - * @param {Object} context - The context for the predicate call - * @returns {Map} A map containing truthy results of a provided filter function - */ -function filter(predicate, context) { - context = context || this; - var result = new Map(); - - for (var entry of this.cache.entries()) { - var key = entry[0]; - var value = entry[1]; - - if (predicate.call(context, value, key)) { - result.set(key, value); - } - } - return result; -} - -MemoryCache.prototype.put = put; -MemoryCache.prototype.get = get; -MemoryCache.prototype.del = del; -MemoryCache.prototype.clear = clear; -MemoryCache.prototype.killTimer = killTimer; -MemoryCache.prototype.size = size; -MemoryCache.prototype.hits = hits; -MemoryCache.prototype.misses = misses; -MemoryCache.prototype.keys = keys; -MemoryCache.prototype.setDebug = setDebug; -MemoryCache.prototype.toArray = toArray; -MemoryCache.prototype.map = map; -MemoryCache.prototype.filter = filter; - -module.exports = MemoryCache; \ No newline at end of file +export default MemoryCache; \ No newline at end of file diff --git a/src/classes/ParseApp.js b/src/classes/ParseApp.js index ee9c1f16f3..e4203adad1 100644 --- a/src/classes/ParseApp.js +++ b/src/classes/ParseApp.js @@ -1,20 +1,23 @@ -var DatabaseProvider = require('./DatabaseProvider'); +import { default as DatabaseProvider } from './DatabaseProvider'; -function ParseApp(args) { +const defaults = { + collectionPrefix: '', + clientKey: '', + javascriptKey: '', + dotNetKey: '', + restAPIKey: '', + fileKey: '', + facebookAppIds: [] +}; + +export class ParseApp { + constructor(args) { if (!args.appId || !args.masterKey) { throw 'You must provide an appId and masterKey!'; } - this.appId = args.appId; - this.masterKey = args.masterKey; - this.collectionPrefix = args.collectionPrefix || ''; - this.clientKey = args.clientKey || ''; - this.javascriptKey = args.javascriptKey || ''; - this.dotNetKey = args.dotNetKey || ''; - this.restAPIKey = args.restAPIKey || ''; - this.fileKey = args.fileKey || 'invalid-file-key'; - this.facebookAppIds = args.facebookAppIds || []; - this.databaseURI = args.databaseURI; + // Merge defaults and arguments + Object.assign(this, defaults, args); // To maintain compatibility. TODO: Remove in v2.1 if (process.env.FACEBOOK_APP_ID) { @@ -25,6 +28,7 @@ function ParseApp(args) { if (this.databaseURI) { DatabaseProvider.registerAppDatabaseURI(this.appId, this.databaseURI); } + } } -exports = module.exports = ParseApp; \ No newline at end of file +export default ParseApp; \ No newline at end of file diff --git a/src/classes/index.js b/src/classes/index.js new file mode 100644 index 0000000000..60bd1659d9 --- /dev/null +++ b/src/classes/index.js @@ -0,0 +1,6 @@ +export { default as BaseProvider } from './BaseProvider'; +export { default as CacheProvider } from './CacheProvider'; +export { default as DatabaseProvider } from './DatabaseProvider'; +export { default as FilesProvider } from './FilesProvider'; +export { default as MemoryCache } from './MemoryCache'; +export { default as ParseApp } from './ParseApp'; \ No newline at end of file diff --git a/src/files.js b/src/files.js index 79eac6b408..dbb601a9f7 100644 --- a/src/files.js +++ b/src/files.js @@ -8,7 +8,7 @@ var bodyParser = require('body-parser'), Parse = require('parse/node').Parse, rack = require('hat').rack(); -var FilesProvider = require('./classes/FilesProvider'); +var FilesProvider = require('./classes/FilesProvider').default; var router = express.Router(); var processCreate = function(req, res, next) { diff --git a/src/index.js b/src/index.js index 854eb59514..81d7440879 100644 --- a/src/index.js +++ b/src/index.js @@ -10,10 +10,10 @@ var batch = require('./batch'), PromiseRouter = require('./PromiseRouter'), httpRequest = require('./httpRequest'); -var ParseApp = require('./classes/ParseApp'); -var CacheProvider = require('./classes/CacheProvider'); -var FilesProvider = require('./classes/FilesProvider'); -var DatabaseProvider = require('./classes/DatabaseProvider'); +var ParseApp = require('./classes/ParseApp').default; +var CacheProvider = require('./classes/CacheProvider').default; +var FilesProvider = require('./classes/FilesProvider').default; +var DatabaseProvider = require('./classes/DatabaseProvider').default; // Mutate the Parse object to add the Cloud Code handlers addParseCloud(); diff --git a/src/interfaces/ServiceProvider.js b/src/interfaces/ServiceProvider.js index a6f9b2c64b..e24c2de68c 100644 --- a/src/interfaces/ServiceProvider.js +++ b/src/interfaces/ServiceProvider.js @@ -3,38 +3,35 @@ * * @interface */ -function ServiceProvider() { -}; - -/** - * Get the adapter - * - * @returns {Object} An adapter instance - */ -ServiceProvider.prototype.getAdapter = function() { +export class ServiceProvider { + /** + * Get the adapter + * + * @returns {Object} An adapter instance + */ + getAdapter() { throw new Error('A service provider must implement getAdapter!'); -} + } -/** - * Set the adapter - * - * @param {Object} An adapter - */ -ServiceProvider.prototype.setAdapter = function() { + /** + * Set the adapter + * + * @param {Object} An adapter + */ + setAdapter() { throw new Error('A service provider must implement setAdapter!'); -} - -/** - * Resolves the adapter from the first parameter - * - * @param {Any} - */ -ServiceProvider.prototype.resolveAdapter = function() { + } + /** + * Resolves the adapter from the first parameter + * + * @param {Any} + */ + resolveAdapter() { throw new Error('A service provider must implement resolveAdapter!'); + } + setup() { + throw new Error('A service provider must implement setup!'); + } } -ServiceProvider.prototype.setup = function() { - throw new Error('A service provider must implement setup!'); -} - -exports = module.exports = ServiceProvider; \ No newline at end of file +export default ServiceProvider; \ No newline at end of file diff --git a/src/middlewares.js b/src/middlewares.js index 668850f842..3125f5eee5 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -1,5 +1,5 @@ var Parse = require('parse/node').Parse; -var CacheProvider = require('./classes/CacheProvider'); +var CacheProvider = require('./classes/CacheProvider').default; var auth = require('./Auth'); var Config = require('./Config'); diff --git a/src/rest.js b/src/rest.js index 238a86c283..6dac565636 100644 --- a/src/rest.js +++ b/src/rest.js @@ -11,7 +11,7 @@ var Parse = require('parse/node').Parse; var RestQuery = require('./RestQuery'); var RestWrite = require('./RestWrite'); var triggers = require('./triggers'); -var CacheProvider = require('./classes/CacheProvider'); +var CacheProvider = require('./classes/CacheProvider').default; // Returns a promise for an object with optional keys 'results' and 'count'. function find(config, auth, className, restWhere, restOptions) { diff --git a/src/testing-routes.js b/src/testing-routes.js index f8c7db4ac5..0bdd99a59d 100644 --- a/src/testing-routes.js +++ b/src/testing-routes.js @@ -4,7 +4,7 @@ var express = require('express'), middlewares = require('./middlewares'), rack = require('hat').rack(); -var CacheProvider = require('./classes/CacheProvider'); +var CacheProvider = require('./classes/CacheProvider').default; var router = express.Router(); // creates a unique app in the cache, with a collection prefix From 9405e3540e102925902eaf49cc526318489f7f09 Mon Sep 17 00:00:00 2001 From: Alexander Mays Date: Tue, 9 Feb 2016 04:57:36 -0500 Subject: [PATCH 11/14] Removed deprecated FilesAdapter and DatabaseAdapter Signed-off-by: Alexander Mays --- src/DatabaseAdapter.js | 56 ------------------------------------------ 1 file changed, 56 deletions(-) delete mode 100644 src/DatabaseAdapter.js diff --git a/src/DatabaseAdapter.js b/src/DatabaseAdapter.js deleted file mode 100644 index 1c99f7ad83..0000000000 --- a/src/DatabaseAdapter.js +++ /dev/null @@ -1,56 +0,0 @@ -// Database Adapter -// -// Allows you to change the underlying database. -// -// Adapter classes must implement the following methods: -// * a constructor with signature (connectionString, optionsObject) -// * connect() -// * loadSchema() -// * create(className, object) -// * find(className, query, options) -// * update(className, query, update, options) -// * destroy(className, query, options) -// * This list is incomplete and the database process is not fully modularized. -// -// Default is ExportAdapter, which uses mongo. - -var ExportAdapter = require('./ExportAdapter'); -var CacheProvider = require('./classes/CacheProvider'); -var adapter = ExportAdapter; -var dbConnections = {}; -var databaseURI = 'mongodb://localhost:27017/parse'; -var appDatabaseURIs = {}; - -function setAdapter(databaseAdapter) { - adapter = databaseAdapter; -} - -function setDatabaseURI(uri) { - databaseURI = uri; -} - -function setAppDatabaseURI(appId, uri) { - appDatabaseURIs[appId] = uri; -} - -function getDatabaseConnection(appId) { - var cache = CacheProvider.getAdapter(); - if (dbConnections[appId]) { - return dbConnections[appId]; - } - - var dbURI = (appDatabaseURIs[appId] ? appDatabaseURIs[appId] : databaseURI); - dbConnections[appId] = new adapter(dbURI, { - collectionPrefix: cache.get(appId)['collectionPrefix'] - }); - dbConnections[appId].connect(); - return dbConnections[appId]; -} - -module.exports = { - dbConnections: dbConnections, - getDatabaseConnection: getDatabaseConnection, - setAdapter: setAdapter, - setDatabaseURI: setDatabaseURI, - setAppDatabaseURI: setAppDatabaseURI -}; From cf0247f04d958793e76def86bbb60ac42d870d6d Mon Sep 17 00:00:00 2001 From: Alexander Mays Date: Tue, 9 Feb 2016 05:36:57 -0500 Subject: [PATCH 12/14] Prevent crashing by adding default args Signed-off-by: Alexander Mays --- src/classes/MemoryCache.js | 4 +--- src/classes/ParseApp.js | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/classes/MemoryCache.js b/src/classes/MemoryCache.js index 076a6d6081..f52425a111 100644 --- a/src/classes/MemoryCache.js +++ b/src/classes/MemoryCache.js @@ -9,9 +9,7 @@ export class MemoryCache { * @param {Object} options - An object of default options * @param {String} [options.defaultTtl=600000] - The number of milliseconds to use as the default time-to-live of a cache entry */ - constructor(options) { - options = options || {}; - + constructor(options = {}) { this.cache = new Map(); this.debug = false; this.hitCount = 0; diff --git a/src/classes/ParseApp.js b/src/classes/ParseApp.js index e4203adad1..173ced2281 100644 --- a/src/classes/ParseApp.js +++ b/src/classes/ParseApp.js @@ -11,7 +11,7 @@ const defaults = { }; export class ParseApp { - constructor(args) { + constructor(args = {}) { if (!args.appId || !args.masterKey) { throw 'You must provide an appId and masterKey!'; } From 20b7835e013a815aff5933fb2b428881d261a699 Mon Sep 17 00:00:00 2001 From: Alexander Mays Date: Tue, 9 Feb 2016 06:31:53 -0500 Subject: [PATCH 13/14] Updated bin/parse-server to wrap the options as an app { ... app: { options } ... } Signed-off-by: Alexander Mays --- bin/parse-server | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/bin/parse-server b/bin/parse-server index 7ec1792721..b0fa4bccd2 100755 --- a/bin/parse-server +++ b/bin/parse-server @@ -10,25 +10,27 @@ if (process.env.PARSE_SERVER_OPTIONS) { options = JSON.parse(process.env.PARSE_SERVER_OPTIONS); } else { - - options.databaseURI = process.env.PARSE_SERVER_DATABASE_URI; - options.cloud = process.env.PARSE_SERVER_CLOUD_CODE_MAIN; - options.collectionPrefix = process.env.PARSE_SERVER_COLLECTION_PREFIX; + options.app = {}; + options.app.databaseURI = process.env.PARSE_SERVER_DATABASE_URI; + options.app.cloud = process.env.PARSE_SERVER_CLOUD_CODE_MAIN; + options.app.collectionPrefix = process.env.PARSE_SERVER_COLLECTION_PREFIX; // Keys and App ID - options.appId = process.env.PARSE_SERVER_APPLICATION_ID; - options.clientKey = process.env.PARSE_SERVER_CLIENT_KEY; - options.restAPIKey = process.env.PARSE_SERVER_REST_API_KEY; - options.dotNetKey = process.env.PARSE_SERVER_DOTNET_KEY; - options.javascriptKey = process.env.PARSE_SERVER_JAVASCRIPT_KEY; - options.masterKey = process.env.PARSE_SERVER_MASTER_KEY; - options.fileKey = process.env.PARSE_SERVER_FILE_KEY; + options.app.appId = process.env.PARSE_SERVER_APPLICATION_ID; + options.app.clientKey = process.env.PARSE_SERVER_CLIENT_KEY; + options.app.restAPIKey = process.env.PARSE_SERVER_REST_API_KEY; + options.app.dotNetKey = process.env.PARSE_SERVER_DOTNET_KEY; + options.app.javascriptKey = process.env.PARSE_SERVER_JAVASCRIPT_KEY; + options.app.dotNetKey = process.env.PARSE_SERVER_DOTNET_KEY; + options.app.masterKey = process.env.PARSE_SERVER_MASTER_KEY; + options.app.fileKey = process.env.PARSE_SERVER_FILE_KEY; + // Comma separated list of facebook app ids var facebookAppIds = process.env.PARSE_SERVER_FACEBOOK_APP_IDS; if (facebookAppIds) { facebookAppIds = facebookAppIds.split(","); - options.facebookAppIds = facebookAppIds; + options.app.facebookAppIds = facebookAppIds; } } From 309775d7182cb4b19ecc10224922f9a6828b0011 Mon Sep 17 00:00:00 2001 From: Alexander Mays Date: Tue, 9 Feb 2016 18:04:30 -0500 Subject: [PATCH 14/14] Decoupled providers and default adapters. Signed-off-by: Alexander Mays --- spec/helper.js | 2 +- src/classes/BaseProvider.js | 5 +++-- src/classes/CacheProvider.js | 7 +------ src/classes/DatabaseProvider.js | 15 ++++---------- src/classes/FilesProvider.js | 7 +------ src/files.js | 10 ++++----- src/index.js | 36 ++++++++++++++++++++++++++------- 7 files changed, 44 insertions(+), 38 deletions(-) diff --git a/spec/helper.js b/spec/helper.js index ab42b4a310..ad5cd563cb 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -12,7 +12,7 @@ var cloudMain = process.env.CLOUD_CODE_MAIN || './cloud/main.js'; var config = { database: { - databaseURI: databaseURI, + databaseURI: databaseURI || "mongodb://localhost:27017/parse", /** adapter: "../ExportAdapter" */ }, cache: { diff --git a/src/classes/BaseProvider.js b/src/classes/BaseProvider.js index 10e3294a89..97aac51d57 100644 --- a/src/classes/BaseProvider.js +++ b/src/classes/BaseProvider.js @@ -56,8 +56,9 @@ export class BaseProvider { return adapter; } - setup (config = {}) { - const adapter = this.resolveAdapter(config.adapter || this.DEFAULT_ADAPTER, config.options); + setup (config = {}, defaultConfig = {}) { + this.config = Object.assign(defaultConfig, config); + const adapter = this.resolveAdapter(this.config.adapter || this.DEFAULT_ADAPTER, this.config.options); this.setAdapter(adapter); } } diff --git a/src/classes/CacheProvider.js b/src/classes/CacheProvider.js index 5ecdbf6736..f47f65ea35 100644 --- a/src/classes/CacheProvider.js +++ b/src/classes/CacheProvider.js @@ -7,11 +7,6 @@ import { default as BaseProvider } from './BaseProvider'; * @extends {BaseProvider} * @param {Object} adapter - A cache adapter */ -export class CacheProvider extends BaseProvider { - constructor() { - super(...arguments); - this.DEFAULT_ADAPTER = './MemoryCache'; - } -} +export class CacheProvider extends BaseProvider {} export default new CacheProvider(); \ No newline at end of file diff --git a/src/classes/DatabaseProvider.js b/src/classes/DatabaseProvider.js index fcac1dd246..b23ff775c5 100644 --- a/src/classes/DatabaseProvider.js +++ b/src/classes/DatabaseProvider.js @@ -1,20 +1,13 @@ import { default as BaseProvider } from './BaseProvider'; import { default as CacheProvider } from './CacheProvider'; -const DEFAULT_URI = "mongodb://localhost:27017/parse"; - export class DatabaseProvider extends BaseProvider { - constructor() { - super(...arguments); - this.DEFAULT_ADAPTER = '../ExportAdapter'; - } - - setup(config = {}) { - this.dbConnections = config.dbConnections || this.dbConnections || {}; - this.databaseURI = config.defaultURI || DEFAULT_URI; - this.appDatabaseURIs = config.appDatabaseURIs || {}; + setup(config = {}, defaultConfig = {}) { super.setup(...arguments); + this.dbConnections = this.dbConnections || {}; + this.appDatabaseURIs = this.appDatabaseURIs || {}; + this.databaseURI = this.config.databaseURI || this.databaseURI; } // TODO: Reimplement this whenever @Flovilmart finishes running CloudCode in subprocesses diff --git a/src/classes/FilesProvider.js b/src/classes/FilesProvider.js index 8e3b996c53..004b2b6750 100644 --- a/src/classes/FilesProvider.js +++ b/src/classes/FilesProvider.js @@ -1,10 +1,5 @@ import { default as BaseProvider } from './BaseProvider'; -export class FilesProvider extends BaseProvider { - constructor() { - super(...arguments); - this.DEFAULT_ADAPTER = '../GridStoreAdapter' - } -} +export class FilesProvider extends BaseProvider {} export default new FilesProvider(); \ No newline at end of file diff --git a/src/files.js b/src/files.js index dbb601a9f7..3906b84b49 100644 --- a/src/files.js +++ b/src/files.js @@ -8,7 +8,8 @@ var bodyParser = require('body-parser'), Parse = require('parse/node').Parse, rack = require('hat').rack(); -var FilesProvider = require('./classes/FilesProvider').default; +import { default as FilesProvider } from './classes/FilesProvider'; + var router = express.Router(); var processCreate = function(req, res, next) { @@ -44,12 +45,11 @@ var processCreate = function(req, res, next) { if (!hasExtension && contentType && mime.extension(contentType)) { extension = '.' + mime.extension(contentType); } - var filename = rack() + '_' + req.params.filename + extension; - FilesAdapter.create(req.config, filename, req.body) + FilesAdapter.createFileAsync(req.config, filename, req.body) .then(() => { res.status(201); - var location = FilesAdapter.location(req.config, req, filename); + var location = FilesAdapter.getFileLocation(req.config, req, filename); res.set('Location', location); res.json({ url: location, name: filename }); }).catch((error) => { @@ -62,7 +62,7 @@ var processCreate = function(req, res, next) { var processGet = function(req, res) { var FilesAdapter = FilesProvider.getAdapter(); var config = new Config(req.params.appId); - FilesAdapter.get(config, req.params.filename) + FilesAdapter.getFileDataAsync(config, req.params.filename) .then((data) => { res.status(200); var contentType = mime.lookup(req.params.filename); diff --git a/src/index.js b/src/index.js index 81d7440879..b1df8dd90e 100644 --- a/src/index.js +++ b/src/index.js @@ -10,10 +10,14 @@ var batch = require('./batch'), PromiseRouter = require('./PromiseRouter'), httpRequest = require('./httpRequest'); -var ParseApp = require('./classes/ParseApp').default; -var CacheProvider = require('./classes/CacheProvider').default; -var FilesProvider = require('./classes/FilesProvider').default; -var DatabaseProvider = require('./classes/DatabaseProvider').default; +import { default as ParseApp } from './classes/ParseApp'; +import { default as CacheProvider } from './classes/CacheProvider'; +import { default as FilesProvider } from './classes/FilesProvider'; +import { default as DatabaseProvider } from './classes/DatabaseProvider'; + +import { default as DEFAULT_CACHE_ADAPTER } from './classes/MemoryCache'; +import { default as DEFAULT_FILES_ADAPTER } from './GridStoreAdapter'; +import { default as DEFAULT_DATABASE_ADAPTER } from './ExportAdapter'; // Mutate the Parse object to add the Cloud Code handlers addParseCloud(); @@ -41,11 +45,29 @@ addParseCloud(); // "restAPIKey": optional key from Parse dashboard // "javascriptKey": optional key from Parse dashboard +const SERVER_DEFAULT_CONFIG = { + "cache": { + adapter: DEFAULT_CACHE_ADAPTER, + options: { + defaultTtl: 10 * 60 * 1000 // 10 min in ms + } + }, + "files": { + adapter: DEFAULT_FILES_ADAPTER, + options: {} + }, + "database": { + adapter: DEFAULT_DATABASE_ADAPTER, + databaseURI: "mongodb://localhost:27017/parse", + options: {} + } +} + function ParseServer(args) { // Setup providers - CacheProvider.setup(args.cache); - FilesProvider.setup(args.files); - DatabaseProvider.setup(args.database); + CacheProvider.setup(args.cache, SERVER_DEFAULT_CONFIG.cache); + FilesProvider.setup(args.files, SERVER_DEFAULT_CONFIG.files); + DatabaseProvider.setup(args.database, SERVER_DEFAULT_CONFIG.database); // Instantiate the app var app = new ParseApp(args.app);