Adding Caching Adapter, allows caching of _Role and _User queries (fixes #168) (#1664)

* Adding Caching Adapter, allows caching of _Role and _User queries.
This commit is contained in:
Blayne Chard
2016-05-18 12:12:30 +12:00
parent 5d887e18f0
commit 8c09c3dae1
18 changed files with 526 additions and 134 deletions

View 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.');
});
});
});

View 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);
});
});

View 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);
})
});

View File

@@ -63,7 +63,7 @@ const setServerConfiguration = configuration => {
DatabaseAdapter.clearDatabaseSettings(); DatabaseAdapter.clearDatabaseSettings();
currentConfiguration = configuration; currentConfiguration = configuration;
server.close(); server.close();
cache.clearCache(); cache.clear();
app = express(); app = express();
api = new ParseServer(configuration); api = new ParseServer(configuration);
app.use('/1', api); app.use('/1', api);

View 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() {}
}

View 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;

View 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;

View File

@@ -2,8 +2,6 @@ var deepcopy = require('deepcopy');
var Parse = require('parse/node').Parse; var Parse = require('parse/node').Parse;
var RestQuery = require('./RestQuery'); var RestQuery = require('./RestQuery');
import cache from './cache';
// An Auth object tells you who is requesting something and whether // An Auth object tells you who is requesting something and whether
// the master key was used. // the master key was used.
// userObject is a Parse.User and can be null if there's no user. // 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 }); return new Auth({ config, isMaster: false });
} }
// Returns a promise that resolves to an Auth object // Returns a promise that resolves to an Auth object
var getAuthForSessionToken = function({ config, sessionToken, installationId } = {}) { var getAuthForSessionToken = function({ config, sessionToken, installationId } = {}) {
var cachedUser = cache.users.get(sessionToken); return config.cacheController.user.get(sessionToken).then((userJSON) => {
if (cachedUser) { if (userJSON) {
return Promise.resolve(new Auth({ config, isMaster: false, installationId, user: cachedUser })); let cachedUser = Parse.Object.fromJSON(userJSON);
} 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);
} }
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; expiresAt = results[0].expiresAt ? new Date(results[0].expiresAt.iso) : undefined;
if(expiresAt < now) { if (expiresAt < now) {
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN,
'Session token is expired.'); 'Session token is expired.');
} }
var obj = results[0]['user']; var obj = results[0]['user'];
delete obj.password; delete obj.password;
obj['className'] = '_User'; obj['className'] = '_User';
obj['sessionToken'] = sessionToken; obj['sessionToken'] = sessionToken;
let userObject = Parse.Object.fromJSON(obj); config.cacheController.user.put(sessionToken, obj);
cache.users.set(sessionToken, userObject);
return new Auth({ config, isMaster: false, installationId, user: userObject }); 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 // Iterates through the role tree and compiles a users roles
Auth.prototype._loadRoles = function() { Auth.prototype._loadRoles = function() {
var restWhere = { var cacheAdapter = this.config.cacheController;
'users': { return cacheAdapter.role.get(this.user.id).then((cachedRoles) => {
__type: 'Pointer', if (cachedRoles != null) {
className: '_User', this.fetchedroles = true;
objectId: this.user.id 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 var restWhere = {
return this._getAllRolesNamesForRoleIds(rolesMap.ids, rolesMap.names) 'users': {
.then((roleNames) => { __type: 'Pointer',
this.userRoles = roleNames.map((r) => { className: '_User',
return 'role:' + r; objectId: this.user.id
}); }
this.fetchedRoles = true; };
this.rolePromise = null; // First get the role ids this user is directly a member of
return Promise.resolve(this.userRoles); 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);
});
}); });
}); });
}; };

View File

@@ -2,7 +2,7 @@
// configured. // configured.
// mount is the URL for the root of the API; includes http, domain, etc. // 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) { function removeTrailingSlash(str) {
if (!str) { if (!str) {
@@ -17,7 +17,7 @@ function removeTrailingSlash(str) {
export class Config { export class Config {
constructor(applicationId: string, mount: string) { constructor(applicationId: string, mount: string) {
let DatabaseAdapter = require('./DatabaseAdapter'); let DatabaseAdapter = require('./DatabaseAdapter');
let cacheInfo = cache.apps.get(applicationId); let cacheInfo = AppCache.get(applicationId);
if (!cacheInfo) { if (!cacheInfo) {
return; return;
} }
@@ -38,6 +38,7 @@ export class Config {
this.verifyUserEmails = cacheInfo.verifyUserEmails; this.verifyUserEmails = cacheInfo.verifyUserEmails;
this.appName = cacheInfo.appName; this.appName = cacheInfo.appName;
this.cacheController = cacheInfo.cacheController;
this.hooksController = cacheInfo.hooksController; this.hooksController = cacheInfo.hooksController;
this.filesController = cacheInfo.filesController; this.filesController = cacheInfo.filesController;
this.pushController = cacheInfo.pushController; this.pushController = cacheInfo.pushController;

View 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;

View File

@@ -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 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]; return dbConnections[appId];
} }

View File

@@ -15,8 +15,8 @@ if (!global._babelPolyfill) {
} }
import { logger, import { logger,
configureLogger } from './logger'; configureLogger } from './logger';
import cache from './cache'; import AppCache from './cache';
import Config from './Config'; import Config from './Config';
import parseServerPackage from '../package.json'; import parseServerPackage from '../package.json';
import PromiseRouter from './PromiseRouter'; import PromiseRouter from './PromiseRouter';
@@ -24,6 +24,8 @@ import requiredParameter from './requiredParameter';
import { AnalyticsRouter } from './Routers/AnalyticsRouter'; import { AnalyticsRouter } from './Routers/AnalyticsRouter';
import { ClassesRouter } from './Routers/ClassesRouter'; import { ClassesRouter } from './Routers/ClassesRouter';
import { FeaturesRouter } from './Routers/FeaturesRouter'; import { FeaturesRouter } from './Routers/FeaturesRouter';
import { InMemoryCacheAdapter } from './Adapters/Cache/InMemoryCacheAdapter';
import { CacheController } from './Controllers/CacheController';
import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter'; import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter';
import { FilesController } from './Controllers/FilesController'; import { FilesController } from './Controllers/FilesController';
import { FilesRouter } from './Routers/FilesRouter'; import { FilesRouter } from './Routers/FilesRouter';
@@ -104,6 +106,7 @@ class ParseServer {
serverURL = requiredParameter('You must provide a serverURL!'), serverURL = requiredParameter('You must provide a serverURL!'),
maxUploadSize = '20mb', maxUploadSize = '20mb',
verifyUserEmails = false, verifyUserEmails = false,
cacheAdapter,
emailAdapter, emailAdapter,
publicServerURL, publicServerURL,
customPages = { customPages = {
@@ -156,6 +159,8 @@ class ParseServer {
const pushControllerAdapter = loadAdapter(push && push.adapter, ParsePushAdapter, push); const pushControllerAdapter = loadAdapter(push && push.adapter, ParsePushAdapter, push);
const loggerControllerAdapter = loadAdapter(loggerAdapter, FileLoggerAdapter); const loggerControllerAdapter = loadAdapter(loggerAdapter, FileLoggerAdapter);
const emailControllerAdapter = loadAdapter(emailAdapter); const emailControllerAdapter = loadAdapter(emailAdapter);
const cacheControllerAdapter = loadAdapter(cacheAdapter, InMemoryCacheAdapter, {appId: appId});
// We pass the options and the base class for the adatper, // We pass the options and the base class for the adatper,
// Note that passing an instance would work too // Note that passing an instance would work too
const filesController = new FilesController(filesControllerAdapter, appId); const filesController = new FilesController(filesControllerAdapter, appId);
@@ -164,8 +169,9 @@ class ParseServer {
const hooksController = new HooksController(appId, collectionPrefix); const hooksController = new HooksController(appId, collectionPrefix);
const userController = new UserController(emailControllerAdapter, appId, { verifyUserEmails }); const userController = new UserController(emailControllerAdapter, appId, { verifyUserEmails });
const liveQueryController = new LiveQueryController(liveQuery); const liveQueryController = new LiveQueryController(liveQuery);
const cacheController = new CacheController(cacheControllerAdapter, appId);
cache.apps.set(appId, { AppCache.put(appId, {
masterKey: masterKey, masterKey: masterKey,
serverURL: serverURL, serverURL: serverURL,
collectionPrefix: collectionPrefix, collectionPrefix: collectionPrefix,
@@ -175,6 +181,7 @@ class ParseServer {
restAPIKey: restAPIKey, restAPIKey: restAPIKey,
fileKey: fileKey, fileKey: fileKey,
facebookAppIds: facebookAppIds, facebookAppIds: facebookAppIds,
cacheController: cacheController,
filesController: filesController, filesController: filesController,
pushController: pushController, pushController: pushController,
loggerController: loggerController, loggerController: loggerController,
@@ -195,11 +202,11 @@ class ParseServer {
// To maintain compatibility. TODO: Remove in some version that breaks backwards compatability // To maintain compatibility. TODO: Remove in some version that breaks backwards compatability
if (process.env.FACEBOOK_APP_ID) { 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)); Config.validate(AppCache.get(appId));
this.config = cache.apps.get(appId); this.config = AppCache.get(appId);
hooksController.load(); hooksController.load();
} }

View File

@@ -2,7 +2,6 @@
// that writes to the database. // that writes to the database.
// This could be either a "create" or an "update". // This could be either a "create" or an "update".
import cache from './cache';
var SchemaController = require('./Controllers/SchemaController'); var SchemaController = require('./Controllers/SchemaController');
var deepcopy = require('deepcopy'); var deepcopy = require('deepcopy');
@@ -310,6 +309,7 @@ RestWrite.prototype.handleAuthData = function(authData) {
}); });
} }
// The non-third-party parts of User transformation // The non-third-party parts of User transformation
RestWrite.prototype.transformUser = function() { RestWrite.prototype.transformUser = function() {
if (this.className !== '_User') { 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 we're updating a _User object, clear the user cache for the session
if (this.query && this.auth.user && this.auth.user.getSessionToken()) { 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(() => { 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. // Handles the _Session class specialness.
// Does nothing if this isn't an installation object. // Does nothing if this isn't an installation object.
RestWrite.prototype.handleSession = function() { RestWrite.prototype.handleSession = function() {
@@ -716,6 +699,10 @@ RestWrite.prototype.runDatabaseOperation = function() {
return; return;
} }
if (this.className === '_Role') {
this.config.cacheController.role.clear();
}
if (this.className === '_User' && if (this.className === '_User' &&
this.query && this.query &&
!this.auth.couldUpdateUserId(this.query.objectId)) { !this.auth.couldUpdateUserId(this.query.objectId)) {

View File

@@ -1,35 +1,4 @@
/** @flow weak */ import {InMemoryCache} from './Adapters/Cache/InMemoryCache';
export function CacheStore<KeyType, ValueType>() { export var AppCache = new InMemoryCache({ttl: NaN});
let dataStore: {[id:KeyType]:ValueType} = {}; export default AppCache;
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
};

View File

@@ -1,5 +1,5 @@
import cache from './cache'; import AppCache from './cache';
import log from './logger'; import log from './logger';
var Parse = require('parse/node').Parse; var Parse = require('parse/node').Parse;
@@ -36,7 +36,7 @@ function handleParseHeaders(req, res, next) {
var fileViaJSON = false; 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. // See if we can find the app id on the body.
if (req.body instanceof Buffer) { if (req.body instanceof Buffer) {
// The only chance to find the app id is if this is a file // 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 && if (req.body &&
req.body._ApplicationId && req.body._ApplicationId &&
cache.apps.get(req.body._ApplicationId) && AppCache.get(req.body._ApplicationId) &&
(!info.masterKey || cache.apps.get(req.body._ApplicationId).masterKey === info.masterKey) (!info.masterKey || AppCache.get(req.body._ApplicationId).masterKey === info.masterKey)
) { ) {
info.appId = req.body._ApplicationId; info.appId = req.body._ApplicationId;
info.javascriptKey = req.body._JavaScriptKey || ''; info.javascriptKey = req.body._JavaScriptKey || '';
@@ -87,7 +87,7 @@ function handleParseHeaders(req, res, next) {
req.body = new Buffer(base64, 'base64'); 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.config = new Config(info.appId, mount);
req.info = info; req.info = info;

View File

@@ -8,8 +8,7 @@
// things. // things.
var Parse = require('parse/node').Parse; var Parse = require('parse/node').Parse;
import cache from './cache'; import Auth from './Auth';
import Auth from './Auth';
var RestQuery = require('./RestQuery'); var RestQuery = require('./RestQuery');
var RestWrite = require('./RestWrite'); var RestWrite = require('./RestWrite');
@@ -48,7 +47,9 @@ function del(config, auth, className, objectId) {
.then((response) => { .then((response) => {
if (response && response.results && response.results.length) { if (response && response.results && response.results.length) {
response.results[0].className = className; 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]); inflatedObject = Parse.Object.fromJSON(response.results[0]);
// Notify LiveQuery server if possible // Notify LiveQuery server if possible
config.liveQueryController.onAfterDelete(inflatedObject.className, inflatedObject); config.liveQueryController.onAfterDelete(inflatedObject.className, inflatedObject);
@@ -96,6 +97,7 @@ function create(config, auth, className, restObject) {
// Usually, this is just updatedAt. // Usually, this is just updatedAt.
function update(config, auth, className, objectId, restObject) { function update(config, auth, className, objectId, restObject) {
enforceRoleSecurity('update', className, auth); enforceRoleSecurity('update', className, auth);
return Promise.resolve().then(() => { return Promise.resolve().then(() => {
if (triggers.getTrigger(className, triggers.Types.beforeSave, config.applicationId) || if (triggers.getTrigger(className, triggers.Types.beforeSave, config.applicationId) ||
triggers.getTrigger(className, triggers.Types.afterSave, config.applicationId) || triggers.getTrigger(className, triggers.Types.afterSave, config.applicationId) ||

View File

@@ -1,5 +1,5 @@
// testing-routes.js // testing-routes.js
import cache from './cache'; import AppCache from './cache';
import * as middlewares from './middlewares'; import * as middlewares from './middlewares';
import { ParseServer } from './index'; import { ParseServer } from './index';
import { Parse } from 'parse/node'; import { Parse } from 'parse/node';
@@ -47,7 +47,7 @@ function dropApp(req, res) {
return res.status(401).send({ "error": "unauthorized" }); return res.status(401).send({ "error": "unauthorized" });
} }
return req.config.database.deleteEverything().then(() => { return req.config.database.deleteEverything().then(() => {
cache.apps.remove(req.config.applicationId); AppCache.del(req.config.applicationId);
res.status(200).send({}); res.status(200).send({});
}); });
} }

View File

@@ -1,6 +1,6 @@
// triggers.js // triggers.js
import Parse from 'parse/node'; import Parse from 'parse/node';
import cache from './cache'; import AppCache from './cache';
export const Types = { export const Types = {
beforeSave: 'beforeSave', beforeSave: 'beforeSave',