* Adding Caching Adapter, allows caching of _Role and _User queries.
This commit is contained in:
74
spec/CacheController.spec.js
Normal file
74
spec/CacheController.spec.js
Normal file
@@ -0,0 +1,74 @@
|
||||
var CacheController = require('../src/Controllers/CacheController.js').default;
|
||||
|
||||
describe('CacheController', function() {
|
||||
var FakeCacheAdapter;
|
||||
var FakeAppID = 'foo';
|
||||
var KEY = 'hello';
|
||||
|
||||
beforeEach(() => {
|
||||
FakeCacheAdapter = {
|
||||
get: () => Promise.resolve(null),
|
||||
put: jasmine.createSpy('put'),
|
||||
del: jasmine.createSpy('del'),
|
||||
clear: jasmine.createSpy('clear')
|
||||
}
|
||||
|
||||
spyOn(FakeCacheAdapter, 'get').and.callThrough();
|
||||
});
|
||||
|
||||
|
||||
it('should expose role and user caches', (done) => {
|
||||
var cache = new CacheController(FakeCacheAdapter, FakeAppID);
|
||||
|
||||
expect(cache.role).not.toEqual(null);
|
||||
expect(cache.role.get).not.toEqual(null);
|
||||
expect(cache.user).not.toEqual(null);
|
||||
expect(cache.user.get).not.toEqual(null);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
['role', 'user'].forEach((cacheName) => {
|
||||
it('should prefix ' + cacheName + ' cache', () => {
|
||||
var cache = new CacheController(FakeCacheAdapter, FakeAppID)[cacheName];
|
||||
|
||||
cache.put(KEY, 'world');
|
||||
var firstPut = FakeCacheAdapter.put.calls.first();
|
||||
expect(firstPut.args[0]).toEqual([FakeAppID, cacheName, KEY].join(':'));
|
||||
|
||||
cache.get(KEY);
|
||||
var firstGet = FakeCacheAdapter.get.calls.first();
|
||||
expect(firstGet.args[0]).toEqual([FakeAppID, cacheName, KEY].join(':'));
|
||||
|
||||
cache.del(KEY);
|
||||
var firstDel = FakeCacheAdapter.del.calls.first();
|
||||
expect(firstDel.args[0]).toEqual([FakeAppID, cacheName, KEY].join(':'));
|
||||
});
|
||||
});
|
||||
|
||||
it('should clear the entire cache', () => {
|
||||
var cache = new CacheController(FakeCacheAdapter, FakeAppID);
|
||||
|
||||
cache.clear();
|
||||
expect(FakeCacheAdapter.clear.calls.count()).toEqual(1);
|
||||
|
||||
cache.user.clear();
|
||||
expect(FakeCacheAdapter.clear.calls.count()).toEqual(2);
|
||||
|
||||
cache.role.clear();
|
||||
expect(FakeCacheAdapter.clear.calls.count()).toEqual(3);
|
||||
});
|
||||
|
||||
it('should handle cache rejections', (done) => {
|
||||
|
||||
FakeCacheAdapter.get = () => Promise.reject();
|
||||
|
||||
var cache = new CacheController(FakeCacheAdapter, FakeAppID);
|
||||
|
||||
cache.get('foo').then(done, () => {
|
||||
fail('Promise should not be rejected.');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
74
spec/InMemoryCache.spec.js
Normal file
74
spec/InMemoryCache.spec.js
Normal file
@@ -0,0 +1,74 @@
|
||||
const InMemoryCache = require('../src/Adapters/Cache/InMemoryCache').default;
|
||||
|
||||
|
||||
describe('InMemoryCache', function() {
|
||||
var BASE_TTL = {
|
||||
ttl: 10
|
||||
};
|
||||
var NO_EXPIRE_TTL = {
|
||||
ttl: NaN
|
||||
};
|
||||
var KEY = 'hello';
|
||||
var KEY_2 = KEY + '_2';
|
||||
|
||||
var VALUE = 'world';
|
||||
|
||||
|
||||
function wait(sleep) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
setTimeout(resolve, sleep);
|
||||
})
|
||||
}
|
||||
|
||||
it('should destroy a expire items in the cache', (done) => {
|
||||
var cache = new InMemoryCache(BASE_TTL);
|
||||
|
||||
cache.put(KEY, VALUE);
|
||||
|
||||
var value = cache.get(KEY);
|
||||
expect(value).toEqual(VALUE);
|
||||
|
||||
wait(BASE_TTL.ttl * 5).then(() => {
|
||||
value = cache.get(KEY)
|
||||
expect(value).toEqual(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete items', (done) => {
|
||||
var cache = new InMemoryCache(NO_EXPIRE_TTL);
|
||||
cache.put(KEY, VALUE);
|
||||
cache.put(KEY_2, VALUE);
|
||||
expect(cache.get(KEY)).toEqual(VALUE);
|
||||
expect(cache.get(KEY_2)).toEqual(VALUE);
|
||||
|
||||
cache.del(KEY);
|
||||
expect(cache.get(KEY)).toEqual(null);
|
||||
expect(cache.get(KEY_2)).toEqual(VALUE);
|
||||
|
||||
cache.del(KEY_2);
|
||||
expect(cache.get(KEY)).toEqual(null);
|
||||
expect(cache.get(KEY_2)).toEqual(null);
|
||||
done();
|
||||
});
|
||||
|
||||
it('should clear all items', (done) => {
|
||||
var cache = new InMemoryCache(NO_EXPIRE_TTL);
|
||||
cache.put(KEY, VALUE);
|
||||
cache.put(KEY_2, VALUE);
|
||||
|
||||
expect(cache.get(KEY)).toEqual(VALUE);
|
||||
expect(cache.get(KEY_2)).toEqual(VALUE);
|
||||
cache.clear();
|
||||
|
||||
expect(cache.get(KEY)).toEqual(null);
|
||||
expect(cache.get(KEY_2)).toEqual(null);
|
||||
done();
|
||||
});
|
||||
|
||||
it('should deafult TTL to 5 seconds', () => {
|
||||
var cache = new InMemoryCache({});
|
||||
expect(cache.ttl).toEqual(5 * 1000);
|
||||
});
|
||||
|
||||
});
|
||||
59
spec/InMemoryCacheAdapter.spec.js
Normal file
59
spec/InMemoryCacheAdapter.spec.js
Normal file
@@ -0,0 +1,59 @@
|
||||
var InMemoryCacheAdapter = require('../src/Adapters/Cache/InMemoryCacheAdapter').default;
|
||||
|
||||
describe('InMemoryCacheAdapter', function() {
|
||||
var KEY = 'hello';
|
||||
var VALUE = 'world';
|
||||
|
||||
function wait(sleep) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
setTimeout(resolve, sleep);
|
||||
})
|
||||
}
|
||||
|
||||
it('should expose promisifyed methods', (done) => {
|
||||
var cache = new InMemoryCacheAdapter({
|
||||
ttl: NaN
|
||||
});
|
||||
|
||||
var noop = () => {};
|
||||
|
||||
// Verify all methods return promises.
|
||||
Promise.all([
|
||||
cache.put(KEY, VALUE),
|
||||
cache.del(KEY),
|
||||
cache.get(KEY),
|
||||
cache.clear()
|
||||
]).then(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should get/set/clear', (done) => {
|
||||
var cache = new InMemoryCacheAdapter({
|
||||
ttl: NaN
|
||||
});
|
||||
|
||||
cache.put(KEY, VALUE)
|
||||
.then(() => cache.get(KEY))
|
||||
.then((value) => expect(value).toEqual(VALUE))
|
||||
.then(() => cache.clear())
|
||||
.then(() => cache.get(KEY))
|
||||
.then((value) => expect(value).toEqual(null))
|
||||
.then(done);
|
||||
});
|
||||
|
||||
it('should expire after ttl', (done) => {
|
||||
var cache = new InMemoryCacheAdapter({
|
||||
ttl: 10
|
||||
});
|
||||
|
||||
cache.put(KEY, VALUE)
|
||||
.then(() => cache.get(KEY))
|
||||
.then((value) => expect(value).toEqual(VALUE))
|
||||
.then(wait.bind(null, 50))
|
||||
.then(() => cache.get(KEY))
|
||||
.then((value) => expect(value).toEqual(null))
|
||||
.then(done);
|
||||
})
|
||||
|
||||
});
|
||||
@@ -63,7 +63,7 @@ const setServerConfiguration = configuration => {
|
||||
DatabaseAdapter.clearDatabaseSettings();
|
||||
currentConfiguration = configuration;
|
||||
server.close();
|
||||
cache.clearCache();
|
||||
cache.clear();
|
||||
app = express();
|
||||
api = new ParseServer(configuration);
|
||||
app.use('/1', api);
|
||||
|
||||
27
src/Adapters/Cache/CacheAdapter.js
Normal file
27
src/Adapters/Cache/CacheAdapter.js
Normal file
@@ -0,0 +1,27 @@
|
||||
export class CacheAdapter {
|
||||
/**
|
||||
* Get a value in the cache
|
||||
* @param key Cache key to get
|
||||
* @return Promise that will eventually resolve to the value in the cache.
|
||||
*/
|
||||
get(key) {}
|
||||
|
||||
/**
|
||||
* Set a value in the cache
|
||||
* @param key Cache key to set
|
||||
* @param value Value to set the key
|
||||
* @param ttl Optional TTL
|
||||
*/
|
||||
put(key, value, ttl) {}
|
||||
|
||||
/**
|
||||
* Remove a value from the cache.
|
||||
* @param key Cache key to remove
|
||||
*/
|
||||
del(key) {}
|
||||
|
||||
/**
|
||||
* Empty a cache
|
||||
*/
|
||||
clear() {}
|
||||
}
|
||||
66
src/Adapters/Cache/InMemoryCache.js
Normal file
66
src/Adapters/Cache/InMemoryCache.js
Normal file
@@ -0,0 +1,66 @@
|
||||
const DEFAULT_CACHE_TTL = 5 * 1000;
|
||||
|
||||
|
||||
export class InMemoryCache {
|
||||
constructor({
|
||||
ttl = DEFAULT_CACHE_TTL
|
||||
}) {
|
||||
this.ttl = ttl;
|
||||
this.cache = Object.create(null);
|
||||
}
|
||||
|
||||
get(key) {
|
||||
let record = this.cache[key];
|
||||
if (record == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Has Record and isnt expired
|
||||
if (isNaN(record.expire) || record.expire >= Date.now()) {
|
||||
return record.value;
|
||||
}
|
||||
|
||||
// Record has expired
|
||||
delete this.cache[key];
|
||||
return null;
|
||||
}
|
||||
|
||||
put(key, value, ttl = this.ttl) {
|
||||
if (ttl < 0 || isNaN(ttl)) {
|
||||
ttl = NaN;
|
||||
}
|
||||
|
||||
var record = {
|
||||
value: value,
|
||||
expire: ttl + Date.now()
|
||||
}
|
||||
|
||||
if (!isNaN(record.expire)) {
|
||||
record.timeout = setTimeout(() => {
|
||||
this.del(key);
|
||||
}, ttl);
|
||||
}
|
||||
|
||||
this.cache[key] = record;
|
||||
}
|
||||
|
||||
del(key) {
|
||||
var record = this.cache[key];
|
||||
if (record == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (record.timeout) {
|
||||
clearTimeout(record.timeout);
|
||||
}
|
||||
|
||||
delete this.cache[key];
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.cache = Object.create(null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default InMemoryCache;
|
||||
36
src/Adapters/Cache/InMemoryCacheAdapter.js
Normal file
36
src/Adapters/Cache/InMemoryCacheAdapter.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import {InMemoryCache} from './InMemoryCache';
|
||||
|
||||
export class InMemoryCacheAdapter {
|
||||
|
||||
constructor(ctx) {
|
||||
this.cache = new InMemoryCache(ctx)
|
||||
}
|
||||
|
||||
get(key) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let record = this.cache.get(key);
|
||||
if (record == null) {
|
||||
return resolve(null);
|
||||
}
|
||||
|
||||
return resolve(JSON.parse(record));
|
||||
})
|
||||
}
|
||||
|
||||
put(key, value, ttl) {
|
||||
this.cache.put(key, JSON.stringify(value), ttl);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
del(key) {
|
||||
this.cache.del(key);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.cache.clear();
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
export default InMemoryCacheAdapter;
|
||||
131
src/Auth.js
131
src/Auth.js
@@ -2,8 +2,6 @@ var deepcopy = require('deepcopy');
|
||||
var Parse = require('parse/node').Parse;
|
||||
var RestQuery = require('./RestQuery');
|
||||
|
||||
import cache from './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.
|
||||
@@ -42,36 +40,42 @@ function nobody(config) {
|
||||
return new Auth({ config, isMaster: false });
|
||||
}
|
||||
|
||||
|
||||
// Returns a promise that resolves to an Auth object
|
||||
var getAuthForSessionToken = function({ config, sessionToken, installationId } = {}) {
|
||||
var cachedUser = cache.users.get(sessionToken);
|
||||
if (cachedUser) {
|
||||
return Promise.resolve(new Auth({ config, isMaster: false, installationId, user: cachedUser }));
|
||||
}
|
||||
var restOptions = {
|
||||
limit: 1,
|
||||
include: 'user'
|
||||
};
|
||||
var query = new RestQuery(config, master(config), '_Session', { sessionToken }, restOptions);
|
||||
return query.execute().then((response) => {
|
||||
var results = response.results;
|
||||
if (results.length !== 1 || !results[0]['user']) {
|
||||
return nobody(config);
|
||||
return config.cacheController.user.get(sessionToken).then((userJSON) => {
|
||||
if (userJSON) {
|
||||
let cachedUser = Parse.Object.fromJSON(userJSON);
|
||||
return Promise.resolve(new Auth({config, isMaster: false, installationId, user: cachedUser}));
|
||||
}
|
||||
|
||||
var now = new Date(),
|
||||
var restOptions = {
|
||||
limit: 1,
|
||||
include: 'user'
|
||||
};
|
||||
|
||||
var query = new RestQuery(config, master(config), '_Session', {sessionToken}, restOptions);
|
||||
return query.execute().then((response) => {
|
||||
var results = response.results;
|
||||
if (results.length !== 1 || !results[0]['user']) {
|
||||
return nobody(config);
|
||||
}
|
||||
|
||||
var now = new Date(),
|
||||
expiresAt = results[0].expiresAt ? new Date(results[0].expiresAt.iso) : undefined;
|
||||
if(expiresAt < now) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN,
|
||||
'Session token is expired.');
|
||||
}
|
||||
var obj = results[0]['user'];
|
||||
delete obj.password;
|
||||
obj['className'] = '_User';
|
||||
obj['sessionToken'] = sessionToken;
|
||||
let userObject = Parse.Object.fromJSON(obj);
|
||||
cache.users.set(sessionToken, userObject);
|
||||
return new Auth({ config, isMaster: false, installationId, user: userObject });
|
||||
if (expiresAt < now) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN,
|
||||
'Session token is expired.');
|
||||
}
|
||||
var obj = results[0]['user'];
|
||||
delete obj.password;
|
||||
obj['className'] = '_User';
|
||||
obj['sessionToken'] = sessionToken;
|
||||
config.cacheController.user.put(sessionToken, obj);
|
||||
|
||||
let userObject = Parse.Object.fromJSON(obj);
|
||||
return new Auth({config, isMaster: false, installationId, user: userObject});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -92,39 +96,50 @@ Auth.prototype.getUserRoles = function() {
|
||||
|
||||
// Iterates through the role tree and compiles a users roles
|
||||
Auth.prototype._loadRoles = function() {
|
||||
var restWhere = {
|
||||
'users': {
|
||||
__type: 'Pointer',
|
||||
className: '_User',
|
||||
objectId: this.user.id
|
||||
var cacheAdapter = this.config.cacheController;
|
||||
return cacheAdapter.role.get(this.user.id).then((cachedRoles) => {
|
||||
if (cachedRoles != null) {
|
||||
this.fetchedroles = true;
|
||||
return Promise.resolve(cachedRoles);
|
||||
}
|
||||
};
|
||||
// First get the role ids this user is directly a member of
|
||||
var query = new RestQuery(this.config, master(this.config), '_Role',
|
||||
restWhere, {});
|
||||
return query.execute().then((response) => {
|
||||
var results = response.results;
|
||||
if (!results.length) {
|
||||
this.userRoles = [];
|
||||
this.fetchedRoles = true;
|
||||
this.rolePromise = null;
|
||||
return Promise.resolve(this.userRoles);
|
||||
}
|
||||
var rolesMap = results.reduce((m, r) => {
|
||||
m.names.push(r.name);
|
||||
m.ids.push(r.objectId);
|
||||
return m;
|
||||
}, {ids: [], names: []});
|
||||
|
||||
// run the recursive finding
|
||||
return this._getAllRolesNamesForRoleIds(rolesMap.ids, rolesMap.names)
|
||||
.then((roleNames) => {
|
||||
this.userRoles = roleNames.map((r) => {
|
||||
return 'role:' + r;
|
||||
});
|
||||
this.fetchedRoles = true;
|
||||
this.rolePromise = null;
|
||||
return Promise.resolve(this.userRoles);
|
||||
var restWhere = {
|
||||
'users': {
|
||||
__type: 'Pointer',
|
||||
className: '_User',
|
||||
objectId: this.user.id
|
||||
}
|
||||
};
|
||||
// First get the role ids this user is directly a member of
|
||||
var query = new RestQuery(this.config, master(this.config), '_Role', restWhere, {});
|
||||
return query.execute().then((response) => {
|
||||
var results = response.results;
|
||||
if (!results.length) {
|
||||
this.userRoles = [];
|
||||
this.fetchedRoles = true;
|
||||
this.rolePromise = null;
|
||||
|
||||
cacheAdapter.role.put(this.user.id, this.userRoles);
|
||||
return Promise.resolve(this.userRoles);
|
||||
}
|
||||
var rolesMap = results.reduce((m, r) => {
|
||||
m.names.push(r.name);
|
||||
m.ids.push(r.objectId);
|
||||
return m;
|
||||
}, {ids: [], names: []});
|
||||
|
||||
// run the recursive finding
|
||||
return this._getAllRolesNamesForRoleIds(rolesMap.ids, rolesMap.names)
|
||||
.then((roleNames) => {
|
||||
this.userRoles = roleNames.map((r) => {
|
||||
return 'role:' + r;
|
||||
});
|
||||
this.fetchedRoles = true;
|
||||
this.rolePromise = null;
|
||||
|
||||
cacheAdapter.role.put(this.user.id, this.userRoles);
|
||||
return Promise.resolve(this.userRoles);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// configured.
|
||||
// mount is the URL for the root of the API; includes http, domain, etc.
|
||||
|
||||
import cache from './cache';
|
||||
import AppCache from './cache';
|
||||
|
||||
function removeTrailingSlash(str) {
|
||||
if (!str) {
|
||||
@@ -17,7 +17,7 @@ function removeTrailingSlash(str) {
|
||||
export class Config {
|
||||
constructor(applicationId: string, mount: string) {
|
||||
let DatabaseAdapter = require('./DatabaseAdapter');
|
||||
let cacheInfo = cache.apps.get(applicationId);
|
||||
let cacheInfo = AppCache.get(applicationId);
|
||||
if (!cacheInfo) {
|
||||
return;
|
||||
}
|
||||
@@ -38,6 +38,7 @@ export class Config {
|
||||
this.verifyUserEmails = cacheInfo.verifyUserEmails;
|
||||
this.appName = cacheInfo.appName;
|
||||
|
||||
this.cacheController = cacheInfo.cacheController;
|
||||
this.hooksController = cacheInfo.hooksController;
|
||||
this.filesController = cacheInfo.filesController;
|
||||
this.pushController = cacheInfo.pushController;
|
||||
|
||||
75
src/Controllers/CacheController.js
Normal file
75
src/Controllers/CacheController.js
Normal file
@@ -0,0 +1,75 @@
|
||||
import AdaptableController from './AdaptableController';
|
||||
import CacheAdapter from '../Adapters/Cache/CacheAdapter';
|
||||
|
||||
const KEY_SEPARATOR_CHAR = ':';
|
||||
|
||||
function joinKeys(...keys) {
|
||||
return keys.join(KEY_SEPARATOR_CHAR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefix all calls to the cache via a prefix string, useful when grouping Cache by object type.
|
||||
*
|
||||
* eg "Role" or "Session"
|
||||
*/
|
||||
export class SubCache {
|
||||
constructor(prefix, cacheController) {
|
||||
this.prefix = prefix;
|
||||
this.cache = cacheController;
|
||||
}
|
||||
|
||||
get(key) {
|
||||
let cacheKey = joinKeys(this.prefix, key);
|
||||
return this.cache.get(cacheKey);
|
||||
}
|
||||
|
||||
put(key, value, ttl) {
|
||||
let cacheKey = joinKeys(this.prefix, key);
|
||||
return this.cache.put(cacheKey, value, ttl);
|
||||
}
|
||||
|
||||
del(key) {
|
||||
let cacheKey = joinKeys(this.prefix, key);
|
||||
return this.cache.del(cacheKey);
|
||||
}
|
||||
|
||||
clear() {
|
||||
return this.cache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class CacheController extends AdaptableController {
|
||||
|
||||
constructor(adapter, appId, options = {}) {
|
||||
super(adapter, appId, options);
|
||||
|
||||
this.role = new SubCache('role', this);
|
||||
this.user = new SubCache('user', this);
|
||||
}
|
||||
|
||||
get(key) {
|
||||
let cacheKey = joinKeys(this.appId, key);
|
||||
return this.adapter.get(cacheKey).then(null, () => Promise.resolve(null));
|
||||
}
|
||||
|
||||
put(key, value, ttl) {
|
||||
let cacheKey = joinKeys(this.appId, key);
|
||||
return this.adapter.put(cacheKey, value, ttl);
|
||||
}
|
||||
|
||||
del(key) {
|
||||
let cacheKey = joinKeys(this.appId, key);
|
||||
return this.adapter.del(cacheKey);
|
||||
}
|
||||
|
||||
clear() {
|
||||
return this.adapter.clear();
|
||||
}
|
||||
|
||||
expectedAdapterType() {
|
||||
return CacheAdapter;
|
||||
}
|
||||
}
|
||||
|
||||
export default CacheController;
|
||||
@@ -60,7 +60,7 @@ function getDatabaseConnection(appId: string, collectionPrefix: string) {
|
||||
uri: appDatabaseURIs[appId], //may be undefined if the user didn't supply a URI, in which case the default will be used
|
||||
}
|
||||
|
||||
dbConnections[appId] = new DatabaseController(new MongoStorageAdapter(mongoAdapterOptions));
|
||||
dbConnections[appId] = new DatabaseController(new MongoStorageAdapter(mongoAdapterOptions), {appId: appId});
|
||||
|
||||
return dbConnections[appId];
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ if (!global._babelPolyfill) {
|
||||
}
|
||||
|
||||
import { logger,
|
||||
configureLogger } from './logger';
|
||||
import cache from './cache';
|
||||
configureLogger } from './logger';
|
||||
import AppCache from './cache';
|
||||
import Config from './Config';
|
||||
import parseServerPackage from '../package.json';
|
||||
import PromiseRouter from './PromiseRouter';
|
||||
@@ -24,6 +24,8 @@ import requiredParameter from './requiredParameter';
|
||||
import { AnalyticsRouter } from './Routers/AnalyticsRouter';
|
||||
import { ClassesRouter } from './Routers/ClassesRouter';
|
||||
import { FeaturesRouter } from './Routers/FeaturesRouter';
|
||||
import { InMemoryCacheAdapter } from './Adapters/Cache/InMemoryCacheAdapter';
|
||||
import { CacheController } from './Controllers/CacheController';
|
||||
import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter';
|
||||
import { FilesController } from './Controllers/FilesController';
|
||||
import { FilesRouter } from './Routers/FilesRouter';
|
||||
@@ -104,6 +106,7 @@ class ParseServer {
|
||||
serverURL = requiredParameter('You must provide a serverURL!'),
|
||||
maxUploadSize = '20mb',
|
||||
verifyUserEmails = false,
|
||||
cacheAdapter,
|
||||
emailAdapter,
|
||||
publicServerURL,
|
||||
customPages = {
|
||||
@@ -156,6 +159,8 @@ class ParseServer {
|
||||
const pushControllerAdapter = loadAdapter(push && push.adapter, ParsePushAdapter, push);
|
||||
const loggerControllerAdapter = loadAdapter(loggerAdapter, FileLoggerAdapter);
|
||||
const emailControllerAdapter = loadAdapter(emailAdapter);
|
||||
const cacheControllerAdapter = loadAdapter(cacheAdapter, InMemoryCacheAdapter, {appId: appId});
|
||||
|
||||
// We pass the options and the base class for the adatper,
|
||||
// Note that passing an instance would work too
|
||||
const filesController = new FilesController(filesControllerAdapter, appId);
|
||||
@@ -164,8 +169,9 @@ class ParseServer {
|
||||
const hooksController = new HooksController(appId, collectionPrefix);
|
||||
const userController = new UserController(emailControllerAdapter, appId, { verifyUserEmails });
|
||||
const liveQueryController = new LiveQueryController(liveQuery);
|
||||
const cacheController = new CacheController(cacheControllerAdapter, appId);
|
||||
|
||||
cache.apps.set(appId, {
|
||||
AppCache.put(appId, {
|
||||
masterKey: masterKey,
|
||||
serverURL: serverURL,
|
||||
collectionPrefix: collectionPrefix,
|
||||
@@ -175,6 +181,7 @@ class ParseServer {
|
||||
restAPIKey: restAPIKey,
|
||||
fileKey: fileKey,
|
||||
facebookAppIds: facebookAppIds,
|
||||
cacheController: cacheController,
|
||||
filesController: filesController,
|
||||
pushController: pushController,
|
||||
loggerController: loggerController,
|
||||
@@ -195,11 +202,11 @@ class ParseServer {
|
||||
|
||||
// To maintain compatibility. TODO: Remove in some version that breaks backwards compatability
|
||||
if (process.env.FACEBOOK_APP_ID) {
|
||||
cache.apps.get(appId)['facebookAppIds'].push(process.env.FACEBOOK_APP_ID);
|
||||
AppCache.get(appId)['facebookAppIds'].push(process.env.FACEBOOK_APP_ID);
|
||||
}
|
||||
|
||||
Config.validate(cache.apps.get(appId));
|
||||
this.config = cache.apps.get(appId);
|
||||
Config.validate(AppCache.get(appId));
|
||||
this.config = AppCache.get(appId);
|
||||
hooksController.load();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// that writes to the database.
|
||||
// This could be either a "create" or an "update".
|
||||
|
||||
import cache from './cache';
|
||||
var SchemaController = require('./Controllers/SchemaController');
|
||||
var deepcopy = require('deepcopy');
|
||||
|
||||
@@ -310,6 +309,7 @@ RestWrite.prototype.handleAuthData = function(authData) {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// The non-third-party parts of User transformation
|
||||
RestWrite.prototype.transformUser = function() {
|
||||
if (this.className !== '_User') {
|
||||
@@ -320,7 +320,8 @@ RestWrite.prototype.transformUser = function() {
|
||||
|
||||
// If we're updating a _User object, clear the user cache for the session
|
||||
if (this.query && this.auth.user && this.auth.user.getSessionToken()) {
|
||||
cache.users.remove(this.auth.user.getSessionToken());
|
||||
let cacheAdapter = this.config.cacheController;
|
||||
cacheAdapter.user.del(this.auth.user.getSessionToken());
|
||||
}
|
||||
|
||||
return promise.then(() => {
|
||||
@@ -441,24 +442,6 @@ RestWrite.prototype.handleFollowup = function() {
|
||||
}
|
||||
};
|
||||
|
||||
// Handles the _Role class specialness.
|
||||
// Does nothing if this isn't a role object.
|
||||
RestWrite.prototype.handleRole = function() {
|
||||
if (this.response || this.className !== '_Role') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.auth.user && !this.auth.isMaster) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN,
|
||||
'Session token required.');
|
||||
}
|
||||
|
||||
if (!this.data.name) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_ROLE_NAME,
|
||||
'Invalid role name.');
|
||||
}
|
||||
};
|
||||
|
||||
// Handles the _Session class specialness.
|
||||
// Does nothing if this isn't an installation object.
|
||||
RestWrite.prototype.handleSession = function() {
|
||||
@@ -716,6 +699,10 @@ RestWrite.prototype.runDatabaseOperation = function() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.className === '_Role') {
|
||||
this.config.cacheController.role.clear();
|
||||
}
|
||||
|
||||
if (this.className === '_User' &&
|
||||
this.query &&
|
||||
!this.auth.couldUpdateUserId(this.query.objectId)) {
|
||||
|
||||
37
src/cache.js
37
src/cache.js
@@ -1,35 +1,4 @@
|
||||
/** @flow weak */
|
||||
import {InMemoryCache} from './Adapters/Cache/InMemoryCache';
|
||||
|
||||
export function CacheStore<KeyType, ValueType>() {
|
||||
let dataStore: {[id:KeyType]:ValueType} = {};
|
||||
return {
|
||||
get: (key: KeyType): ValueType => {
|
||||
return dataStore[key];
|
||||
},
|
||||
set(key: KeyType, value: ValueType): void {
|
||||
dataStore[key] = value;
|
||||
},
|
||||
remove(key: KeyType): void {
|
||||
delete dataStore[key];
|
||||
},
|
||||
clear(): void {
|
||||
dataStore = {};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const apps = CacheStore();
|
||||
const users = CacheStore();
|
||||
|
||||
//So far used only in tests
|
||||
export function clearCache(): void {
|
||||
apps.clear();
|
||||
users.clear();
|
||||
}
|
||||
|
||||
export default {
|
||||
apps,
|
||||
users,
|
||||
clearCache,
|
||||
CacheStore
|
||||
};
|
||||
export var AppCache = new InMemoryCache({ttl: NaN});
|
||||
export default AppCache;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import cache from './cache';
|
||||
import log from './logger';
|
||||
import AppCache from './cache';
|
||||
import log from './logger';
|
||||
|
||||
var Parse = require('parse/node').Parse;
|
||||
|
||||
@@ -36,7 +36,7 @@ function handleParseHeaders(req, res, next) {
|
||||
|
||||
var fileViaJSON = false;
|
||||
|
||||
if (!info.appId || !cache.apps.get(info.appId)) {
|
||||
if (!info.appId || !AppCache.get(info.appId)) {
|
||||
// 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
|
||||
@@ -51,8 +51,8 @@ function handleParseHeaders(req, res, next) {
|
||||
|
||||
if (req.body &&
|
||||
req.body._ApplicationId &&
|
||||
cache.apps.get(req.body._ApplicationId) &&
|
||||
(!info.masterKey || cache.apps.get(req.body._ApplicationId).masterKey === info.masterKey)
|
||||
AppCache.get(req.body._ApplicationId) &&
|
||||
(!info.masterKey || AppCache.get(req.body._ApplicationId).masterKey === info.masterKey)
|
||||
) {
|
||||
info.appId = req.body._ApplicationId;
|
||||
info.javascriptKey = req.body._JavaScriptKey || '';
|
||||
@@ -87,7 +87,7 @@ function handleParseHeaders(req, res, next) {
|
||||
req.body = new Buffer(base64, 'base64');
|
||||
}
|
||||
|
||||
info.app = cache.apps.get(info.appId);
|
||||
info.app = AppCache.get(info.appId);
|
||||
req.config = new Config(info.appId, mount);
|
||||
req.info = info;
|
||||
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
// things.
|
||||
|
||||
var Parse = require('parse/node').Parse;
|
||||
import cache from './cache';
|
||||
import Auth from './Auth';
|
||||
import Auth from './Auth';
|
||||
|
||||
var RestQuery = require('./RestQuery');
|
||||
var RestWrite = require('./RestWrite');
|
||||
@@ -48,7 +47,9 @@ function del(config, auth, className, objectId) {
|
||||
.then((response) => {
|
||||
if (response && response.results && response.results.length) {
|
||||
response.results[0].className = className;
|
||||
cache.users.remove(response.results[0].sessionToken);
|
||||
|
||||
var cacheAdapter = config.cacheController;
|
||||
cacheAdapter.user.del(response.results[0].sessionToken);
|
||||
inflatedObject = Parse.Object.fromJSON(response.results[0]);
|
||||
// Notify LiveQuery server if possible
|
||||
config.liveQueryController.onAfterDelete(inflatedObject.className, inflatedObject);
|
||||
@@ -96,6 +97,7 @@ function create(config, auth, className, restObject) {
|
||||
// Usually, this is just updatedAt.
|
||||
function update(config, auth, className, objectId, restObject) {
|
||||
enforceRoleSecurity('update', className, auth);
|
||||
|
||||
return Promise.resolve().then(() => {
|
||||
if (triggers.getTrigger(className, triggers.Types.beforeSave, config.applicationId) ||
|
||||
triggers.getTrigger(className, triggers.Types.afterSave, config.applicationId) ||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// testing-routes.js
|
||||
import cache from './cache';
|
||||
import AppCache from './cache';
|
||||
import * as middlewares from './middlewares';
|
||||
import { ParseServer } from './index';
|
||||
import { Parse } from 'parse/node';
|
||||
@@ -47,7 +47,7 @@ function dropApp(req, res) {
|
||||
return res.status(401).send({ "error": "unauthorized" });
|
||||
}
|
||||
return req.config.database.deleteEverything().then(() => {
|
||||
cache.apps.remove(req.config.applicationId);
|
||||
AppCache.del(req.config.applicationId);
|
||||
res.status(200).send({});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// triggers.js
|
||||
import Parse from 'parse/node';
|
||||
import cache from './cache';
|
||||
import AppCache from './cache';
|
||||
|
||||
export const Types = {
|
||||
beforeSave: 'beforeSave',
|
||||
|
||||
Reference in New Issue
Block a user