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; } } 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/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 8670bdd2be..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 DatabaseAdapter = require('../src/DatabaseAdapter'); +var DatabaseProvider = require('../src/classes/DatabaseProvider').default; 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 91bb9a23b4..08d8775f15 100644 --- a/spec/ParseInstallation.spec.js +++ b/spec/ParseInstallation.spec.js @@ -2,14 +2,13 @@ // 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 DatabaseProvider = require('../src/classes/DatabaseProvider').default; 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 244555075a..04d3595102 100644 --- a/spec/RestCreate.spec.js +++ b/spec/RestCreate.spec.js @@ -1,14 +1,13 @@ // 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 DatabaseProvider = require('../src/classes/DatabaseProvider').default; 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/RestQuery.spec.js b/spec/RestQuery.spec.js index b93a07d588..9ca5784b91 100644 --- a/spec/RestQuery.spec.js +++ b/spec/RestQuery.spec.js @@ -1,6 +1,5 @@ // 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'); diff --git a/spec/classes/MemoryCache.spec.js b/spec/classes/MemoryCache.spec.js new file mode 100644 index 0000000000..daf7cd046b --- /dev/null +++ b/spec/classes/MemoryCache.spec.js @@ -0,0 +1,648 @@ +/* global describe, it, before, beforeEach, afterEach */ +'use strict'; + +var MemoryCache = require('../../src/classes/MemoryCache').default; + +var cache = new 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(undefined); + }); + + 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 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 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(undefined); + expect(cache.get('key2')).toBe(undefined); + expect(cache.get('key3')).toBe(undefined); + }); + + 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(undefined); + }); + + 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 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 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 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 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 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'); + }); + }); + + 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() + .map(function(item) { return item.value }) + ).toDeepEqual(['value1', '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').value).toEqual('value1'); + expect(filtered.get('key2')).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() { + 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').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/spec/helper.js b/spec/helper.js index 3e6c6d9853..ad5cd563cb 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 DatabaseProvider = require('../src/classes/DatabaseProvider').default; var express = require('express'); var facebook = require('../src/facebook'); var ParseServer = require('../src/index').ParseServer; @@ -11,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 || "mongodb://localhost:27017/parse", + /** 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); @@ -191,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/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/Auth.js b/src/Auth.js index ad9056549a..ef8809e82f 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -1,8 +1,7 @@ var deepcopy = require('deepcopy'); var Parse = require('parse/node').Parse; var RestQuery = require('./RestQuery'); - -var cache = require('./cache'); +var CacheProvider = require('./classes/CacheProvider').default; // An Auth object tells you who is requesting something and whether // the master key was used. @@ -43,7 +42,9 @@ function nobody(config) { // Returns a promise that resolves to an Auth object var getAuthForSessionToken = function(config, sessionToken) { - var cachedUser = cache.getUser(sessionToken); + 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..f24f8340b7 100644 --- a/src/Config.js +++ b/src/Config.js @@ -1,11 +1,13 @@ // 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('./cache'); - var DatabaseAdapter = require('./DatabaseAdapter'); + var DatabaseProvider = require('./classes/DatabaseProvider').default; + var CacheProvider = require('./classes/CacheProvider').default; + var cache = CacheProvider.getAdapter(); - var cacheInfo = cache.apps[applicationId]; + var cacheInfo = cache.get(applicationId); this.valid = !!cacheInfo; if (!this.valid) { return; @@ -13,7 +15,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/DatabaseAdapter.js b/src/DatabaseAdapter.js deleted file mode 100644 index 4967d5665d..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 adapter = ExportAdapter; -var cache = require('./cache'); -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) { - if (dbConnections[appId]) { - return dbConnections[appId]; - } - - var dbURI = (appDatabaseURIs[appId] ? appDatabaseURIs[appId] : databaseURI); - dbConnections[appId] = new adapter(dbURI, { - collectionPrefix: cache.apps[appId]['collectionPrefix'] - }); - dbConnections[appId].connect(); - return dbConnections[appId]; -} - -module.exports = { - dbConnections: dbConnections, - getDatabaseConnection: getDatabaseConnection, - setAdapter: setAdapter, - setDatabaseURI: setDatabaseURI, - setAppDatabaseURI: setAppDatabaseURI -}; 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/classes/BaseProvider.js b/src/classes/BaseProvider.js new file mode 100644 index 0000000000..97aac51d57 --- /dev/null +++ b/src/classes/BaseProvider.js @@ -0,0 +1,67 @@ +import { default as ServiceProviderInterface } from '../interfaces/ServiceProvider'; +/** + * A base provider class that allows for an abstraction of adapter implementations + * + * @class + * @implements {ServiceProvider} + * @param {Object} adapter - An adapter + */ +export class BaseProvider { + constructor(adapter){ + if (adapter) { + this.adapter = adapter; + } + } + + /** + * Get the adapter + * + * @returns {Object} An adapter instance + */ + getAdapter() { + return this.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 + */ + resolveAdapter(adapter, options) { + // Support passing in adapter paths + if (typeof adapter === 'string') { + adapter = require(adapter); + + // TODO: Figure out a better way to deal with this + if (adapter && adapter.default) + adapter = adapter.default; + } + + // Instantiate the adapter if the class got passed instead of an instance + if (typeof adapter === 'function') { + adapter = new adapter(options); + } + + return adapter; + } + + 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); + } +} + + +export default BaseProvider; \ No newline at end of file diff --git a/src/classes/CacheProvider.js b/src/classes/CacheProvider.js new file mode 100644 index 0000000000..f47f65ea35 --- /dev/null +++ b/src/classes/CacheProvider.js @@ -0,0 +1,12 @@ +import { default as BaseProvider } from './BaseProvider'; + +/** +* Abstract class the provides a reference to an adapter instance (a caching implementation) +* +* @class +* @extends {BaseProvider} +* @param {Object} adapter - A cache adapter +*/ +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 new file mode 100644 index 0000000000..b23ff775c5 --- /dev/null +++ b/src/classes/DatabaseProvider.js @@ -0,0 +1,58 @@ +import { default as BaseProvider } from './BaseProvider'; +import { default as CacheProvider } from './CacheProvider'; + +export class DatabaseProvider extends BaseProvider { + + 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 + registerAppDatabaseURI(appId, uri) { + this.appDatabaseURIs[appId] = uri; + } + + getDatabaseConnections() { + return this.dbConnections; + } + + getDatabaseConnection(appId) { + if (this.dbConnections[appId]) { + return this.dbConnections[appId]; + } + + const cache = CacheProvider.getAdapter(); + const app = cache.get(appId); + + if (!app) { + throw new Error('Application ID provided is not a registered application.'); + } + + const adapterClass = this.getAdapter(); + const dbURI = this.appDatabaseURIs[appId] || this.databaseURI; + const options = { collectionPrefix: app.collectionPrefix }; + + this.dbConnections[appId] = new adapterClass(dbURI, options); + this.dbConnections[appId].connect(); + return this.dbConnections[appId]; + } + + // Overriding resolveAdapter to prevent instantiation + resolveAdapter(adapter, options) { + // Support passing in adapter paths + if (typeof adapter === 'string') { + adapter = require(adapter); + + // TODO: Figure out a better way to deal with this + if (adapter && adapter.default) + adapter = adapter.default; + } + + return adapter; + } +} + +export default new DatabaseProvider(); \ No newline at end of file diff --git a/src/classes/FilesProvider.js b/src/classes/FilesProvider.js new file mode 100644 index 0000000000..004b2b6750 --- /dev/null +++ b/src/classes/FilesProvider.js @@ -0,0 +1,5 @@ +import { default as BaseProvider } from './BaseProvider'; + +export class FilesProvider extends BaseProvider {} + +export default new FilesProvider(); \ No newline at end of file diff --git a/src/classes/MemoryCache.js b/src/classes/MemoryCache.js new file mode 100644 index 0000000000..f52425a111 --- /dev/null +++ b/src/classes/MemoryCache.js @@ -0,0 +1,235 @@ +/** +* In-memory cache using Map for storage +* +* @class +*/ +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 = {}) { + this.cache = new Map(); + this.debug = false; + 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 + */ + 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'); + } + + // 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); + } + + var record = { + value: value, + expire: (ttl + Date.now()) + }; + + if (!isNaN(record.expire) && ttl !== Infinity) { + record.timeout = setTimeout(() => { + this.del(key); + if (timeoutCallback) { + timeoutCallback(key); + } + }, ttl); + } + + this.cache.set(key, record); + + 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) + */ + 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; + } + + /** + * 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; + }; + + /** + * 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 { + this.missCount++; + } + return undefined; + }; + + /** + * @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 + */ + setDebug (bool) { + this.debug = bool; + }; + + /** + * @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 + */ + misses () { + return this.missCount; + }; + + /** + * @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 + */ + 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 + */ + 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; + } + + /** + * @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; + } + +}; + + +export default MemoryCache; \ No newline at end of file diff --git a/src/classes/ParseApp.js b/src/classes/ParseApp.js new file mode 100644 index 0000000000..173ced2281 --- /dev/null +++ b/src/classes/ParseApp.js @@ -0,0 +1,34 @@ +import { default as DatabaseProvider } from './DatabaseProvider'; + +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!'; + } + + // Merge defaults and arguments + Object.assign(this, defaults, args); + + // 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); + } + } +} + +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 86cdbfbe13..3906b84b49 100644 --- a/src/files.js +++ b/src/files.js @@ -8,11 +8,17 @@ var bodyParser = require('body-parser'), Parse = require('parse/node').Parse, rack = require('hat').rack(); -import { getAdapter as getFilesAdapter } from './FilesAdapter'; +import { default as FilesProvider } from './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.')); @@ -39,11 +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; - getFilesAdapter().createFileAsync(req.config, filename, req.body).then(() => { + FilesAdapter.createFileAsync(req.config, filename, req.body) + .then(() => { res.status(201); - var location = getFilesAdapter().getFileLocation(req.config, req, filename); + var location = FilesAdapter.getFileLocation(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.getFileDataAsync(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 48d3e8c9d7..b1df8dd90e 100644 --- a/src/index.js +++ b/src/index.js @@ -2,8 +2,6 @@ var batch = require('./batch'), bodyParser = require('body-parser'), - cache = require('./cache'), - DatabaseAdapter = require('./DatabaseAdapter'), express = require('express'), S3Adapter = require('./S3Adapter'), middlewares = require('./middlewares'), @@ -12,8 +10,14 @@ var batch = require('./batch'), PromiseRouter = require('./PromiseRouter'), httpRequest = require('./httpRequest'); -import { setAdapter as setFilesAdapter } from './FilesAdapter'; -import { default as GridStoreAdapter } from './GridStoreAdapter'; +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(); @@ -40,53 +44,56 @@ addParseCloud(); // "dotNetKey": optional key from Parse dashboard // "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) { - if (!args.appId || !args.masterKey) { - throw 'You must provide an appId and masterKey!'; - } + // Setup providers + 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); - 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 "argument 'cloud' must either be a string or a function"; + throw new Error("argument 'cloud' must either be a string or a function"); } - } - cache.apps[args.appId] = { - 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) { - cache.apps[args.appId]['facebookAppIds'].push(process.env.FACEBOOK_APP_ID); - } + // Cache the application information indefinitely + var cache = CacheProvider.getAdapter(); + cache.put(app.appId, app, 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; } diff --git a/src/interfaces/ServiceProvider.js b/src/interfaces/ServiceProvider.js new file mode 100644 index 0000000000..e24c2de68c --- /dev/null +++ b/src/interfaces/ServiceProvider.js @@ -0,0 +1,37 @@ +/** + * Interface for service providers + * + * @interface + */ +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 + */ + setAdapter() { + throw new Error('A service provider must implement setAdapter!'); + } + /** + * 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!'); + } +} + +export default ServiceProvider; \ No newline at end of file diff --git a/src/middlewares.js b/src/middlewares.js index bb2512391a..3125f5eee5 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -1,7 +1,6 @@ var Parse = require('parse/node').Parse; - +var CacheProvider = require('./classes/CacheProvider').default; 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,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 = 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 +27,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 +38,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 +75,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..6dac565636 100644 --- a/src/rest.js +++ b/src/rest.js @@ -8,11 +8,10 @@ // things. var Parse = require('parse/node').Parse; - -var cache = require('./cache'); var RestQuery = require('./RestQuery'); var RestWrite = require('./RestWrite'); var triggers = require('./triggers'); +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) { @@ -37,6 +36,7 @@ function del(config, auth, className, objectId) { enforceRoleSecurity('delete', className, auth); var inflatedObject; + 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..0bdd99a59d 100644 --- a/src/testing-routes.js +++ b/src/testing-routes.js @@ -1,19 +1,21 @@ // testing-routes.js var express = require('express'), - cache = require('./cache'), middlewares = require('./middlewares'), rack = require('hat').rack(); +var CacheProvider = require('./classes/CacheProvider').default; var router = express.Router(); // creates a unique app in the cache, with a collection prefix function createApp(req, res) { + var cache = CacheProvider.getAdapter(); var appId = rack(); - cache.apps[appId] = { + cache.put(appId, { 'collectionPrefix': appId + '_', 'masterKey': 'master' - }; + }, Infinity); + 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({}); }); }