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();
currentConfiguration = configuration;
server.close();
cache.clearCache();
cache.clear();
app = express();
api = new ParseServer(configuration);
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 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);
});
});
});
};

View File

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

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
}
dbConnections[appId] = new DatabaseController(new MongoStorageAdapter(mongoAdapterOptions));
dbConnections[appId] = new DatabaseController(new MongoStorageAdapter(mongoAdapterOptions), {appId: appId});
return dbConnections[appId];
}

View File

@@ -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();
}

View File

@@ -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)) {

View File

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

View File

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

View File

@@ -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) ||

View File

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

View File

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