Improve single schema cache (#7214)

* Initial Commit

* fix flaky test

* temporary set ci timeout

* turn off ci check

* fix postgres tests

* fix tests

* node flaky test

* remove improvements

* Update SchemaPerformance.spec.js

* fix tests

* revert ci

* Create Singleton Object

* properly clear cache testing

* Cleanup

* remove fit

* try PushController.spec

* try push test rewrite

* try push enqueue time

* Increase test timeout

* remove pg server creation test

* xit push tests

* more xit

* remove skipped tests

* Fix conflicts

* reduce ci timeout

* fix push tests

* Revert "fix push tests"

This reverts commit 05aba62f1cbbca7d5d3e80b9444529f59407cb56.

* improve initialization

* fix flaky tests

* xit flaky test

* Update CHANGELOG.md

* enable debug logs

* Update LogsRouter.spec.js

* create initial indexes in series

* lint

* horizontal scaling documentation

* Update Changelog

* change horizontalScaling db option

* Add enableSchemaHooks option

* move enableSchemaHooks to databaseOptions
This commit is contained in:
Diamond Lewis
2021-03-16 16:05:36 -05:00
committed by GitHub
parent 32fc45d2d2
commit a02014f557
38 changed files with 673 additions and 937 deletions

View File

@@ -89,6 +89,9 @@ ___
## Unreleased (Master Branch) ## Unreleased (Master Branch)
[Full Changelog](https://github.com/parse-community/parse-server/compare/4.5.0...master) [Full Changelog](https://github.com/parse-community/parse-server/compare/4.5.0...master)
### Breaking Changes ### Breaking Changes
Leveraging database real-time hooks, schema caching has been drastically improved. These improvements allows for reduced calls to the DB, faster queries and prevention of memory leaks. A breaking change can occur if you are horizontally scaling Parse Server (multiple Parse Server instances connecting to the same DB). Set `databaseOptions: { enableSchemaHooks: true }` parameter in [Parse Server Options](https://parseplatform.org/parse-server/api/master/ParseServerOptions.html) (`enableSingleSchemaCache` and `schemaCacheTTL` have been removed). If you are horizontal scaling instances connected to MongoDB, you must use replica set clusters with WiredTiger, see [ChangeStream](https://docs.mongodb.com/manual/changeStreams/#availability)
The new schema cache uses a singleton object that is stored in-memory. In a horizontally scaled environment, if you update the schema in one instance the DB hooks will update the schema in all other instances. `databaseOptions: { enableSchemaHooks: true }` enables the DB hooks. If you have multiple server instances but `databaseOptions: { enableSchemaHooks: false }`, your schema maybe out of sync in your instances (resyncing will happen if an instance restarts). (Diamond Lewis, SebC) [#7214](https://github.com/parse-community/parse-server/issues/7214)
- Added file upload restriction. File upload is now only allowed for authenticated users by default for improved security. To allow file upload also for Anonymous Users or Public, set the `fileUpload` parameter in the [Parse Server Options](https://parseplatform.org/parse-server/api/master/ParseServerOptions.html) (dblythy, Manuel Trezza) [#7071](https://github.com/parse-community/parse-server/pull/7071) - Added file upload restriction. File upload is now only allowed for authenticated users by default for improved security. To allow file upload also for Anonymous Users or Public, set the `fileUpload` parameter in the [Parse Server Options](https://parseplatform.org/parse-server/api/master/ParseServerOptions.html) (dblythy, Manuel Trezza) [#7071](https://github.com/parse-community/parse-server/pull/7071)
### Notable Changes ### Notable Changes
- Added Parse Server Security Check to report weak security settings (Manuel Trezza, dblythy) [#7247](https://github.com/parse-community/parse-server/issues/7247) - Added Parse Server Security Check to report weak security settings (Manuel Trezza, dblythy) [#7247](https://github.com/parse-community/parse-server/issues/7247)

View File

@@ -53,6 +53,7 @@ function getENVPrefix(iface) {
'PasswordPolicyOptions' : 'PARSE_SERVER_PASSWORD_POLICY_', 'PasswordPolicyOptions' : 'PARSE_SERVER_PASSWORD_POLICY_',
'FileUploadOptions' : 'PARSE_SERVER_FILE_UPLOAD_', 'FileUploadOptions' : 'PARSE_SERVER_FILE_UPLOAD_',
'SecurityOptions': 'PARSE_SERVER_SECURITY_', 'SecurityOptions': 'PARSE_SERVER_SECURITY_',
'DatabaseOptions': 'PARSE_SERVER_DATABASE_'
} }
if (options[iface.id.name]) { if (options[iface.id.name]) {
return options[iface.id.name] return options[iface.id.name]
@@ -168,7 +169,7 @@ function parseDefaultValue(elt, value, t) {
if (type == 'NumberOrBoolean') { if (type == 'NumberOrBoolean') {
literalValue = t.numericLiteral(parsers.numberOrBoolParser('')(value)); literalValue = t.numericLiteral(parsers.numberOrBoolParser('')(value));
} }
const literalTypes = ['Object', 'SecurityOptions', 'PagesRoute', 'IdempotencyOptions','FileUploadOptions','CustomPagesOptions', 'PagesCustomUrlsOptions', 'PagesOptions']; const literalTypes = ['Object', 'SecurityOptions', 'PagesRoute', 'IdempotencyOptions','FileUploadOptions','CustomPagesOptions', 'PagesCustomUrlsOptions', 'PagesOptions', 'DatabaseOptions'];
if (literalTypes.includes(type)) { if (literalTypes.includes(type)) {
const object = parsers.objectParser(value); const object = parsers.objectParser(value);
const props = Object.keys(object).map((key) => { const props = Object.keys(object).map((key) => {

View File

@@ -1,58 +0,0 @@
const auth = require('../lib/Auth');
const Config = require('../lib/Config');
const rest = require('../lib/rest');
describe('Enable single schema cache', () => {
beforeEach(done => {
reconfigureServer({
enableSingleSchemaCache: true,
schemaCacheTTL: 30000,
}).then(() => {
done();
});
});
it('can perform multiple create and query operations', done => {
let config = fakeRequestForConfig();
let nobody = auth.nobody(config);
rest
.create(config, nobody, 'Foo', { type: 1 })
.then(() => {
config = fakeRequestForConfig();
nobody = auth.nobody(config);
return rest.create(config, nobody, 'Foo', { type: 2 });
})
.then(() => {
config = fakeRequestForConfig();
nobody = auth.nobody(config);
return rest.create(config, nobody, 'Bar');
})
.then(() => {
config = fakeRequestForConfig();
nobody = auth.nobody(config);
return rest.find(config, nobody, 'Bar', { type: 1 });
})
.then(
() => {
fail('Should throw error');
done();
},
error => {
config = fakeRequestForConfig();
nobody = auth.nobody(config);
expect(error).toBeDefined();
return rest.find(config, nobody, 'Foo', { type: 1 });
}
)
.then(response => {
config = fakeRequestForConfig();
nobody = auth.nobody(config);
expect(response.results.length).toEqual(1);
done();
});
});
});
const fakeRequestForConfig = function () {
return Config.get('test');
};

View File

@@ -8,7 +8,9 @@ const WinstonLoggerAdapter = require('../lib/Adapters/Logger/WinstonLoggerAdapte
const loggerController = new LoggerController(new WinstonLoggerAdapter()); const loggerController = new LoggerController(new WinstonLoggerAdapter());
describe('LogsRouter', () => { describe_only(() => {
return process.env.PARSE_SERVER_LOG_LEVEL !== 'debug';
})('LogsRouter', () => {
it('can check valid master key of request', done => { it('can check valid master key of request', done => {
// Make mock request // Make mock request
const request = { const request = {

View File

@@ -18,6 +18,7 @@ const fakeClient = {
describe_only_db('mongo')('MongoStorageAdapter', () => { describe_only_db('mongo')('MongoStorageAdapter', () => {
beforeEach(done => { beforeEach(done => {
new MongoStorageAdapter({ uri: databaseURI }).deleteAllClasses().then(done, fail); new MongoStorageAdapter({ uri: databaseURI }).deleteAllClasses().then(done, fail);
Config.get(Parse.applicationId).schemaCache.clear();
}); });
it('auto-escapes symbols in auth information', () => { it('auto-escapes symbols in auth information', () => {
@@ -314,6 +315,8 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
await user.signUp(); await user.signUp();
const database = Config.get(Parse.applicationId).database; const database = Config.get(Parse.applicationId).database;
await database.adapter.dropAllIndexes('_User');
const preIndexPlan = await database.find( const preIndexPlan = await database.find(
'_User', '_User',
{ username: 'bugs' }, { username: 'bugs' },
@@ -546,5 +549,33 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
}); });
}); });
}); });
describe('watch _SCHEMA', () => {
it('should change', async done => {
const adapter = new MongoStorageAdapter({
uri: databaseURI,
collectionPrefix: '',
mongoOptions: { enableSchemaHooks: true },
});
await reconfigureServer({ databaseAdapter: adapter });
expect(adapter.enableSchemaHooks).toBe(true);
spyOn(adapter, '_onchange');
const schema = {
fields: {
array: { type: 'Array' },
object: { type: 'Object' },
date: { type: 'Date' },
},
};
await adapter.createClass('Stuff', schema);
const myClassSchema = await adapter.getClass('Stuff');
expect(myClassSchema).toBeDefined();
setTimeout(() => {
expect(adapter._onchange).toHaveBeenCalled();
done();
}, 5000);
});
});
} }
}); });

View File

@@ -30,9 +30,7 @@ describe('ParseGraphQLController', () => {
beforeEach(async () => { beforeEach(async () => {
if (!parseServer) { if (!parseServer) {
parseServer = await global.reconfigureServer({ parseServer = await global.reconfigureServer();
schemaCacheTTL: 100,
});
databaseController = parseServer.config.databaseController; databaseController = parseServer.config.databaseController;
cacheController = parseServer.config.cacheController; cacheController = parseServer.config.cacheController;

View File

@@ -10,9 +10,7 @@ describe('ParseGraphQLSchema', () => {
const appId = 'test'; const appId = 'test';
beforeEach(async () => { beforeEach(async () => {
parseServer = await global.reconfigureServer({ parseServer = await global.reconfigureServer();
schemaCacheTTL: 100,
});
databaseController = parseServer.config.databaseController; databaseController = parseServer.config.databaseController;
parseGraphQLController = parseServer.config.parseGraphQLController; parseGraphQLController = parseServer.config.parseGraphQLController;
parseGraphQLSchema = new ParseGraphQLSchema({ parseGraphQLSchema = new ParseGraphQLSchema({
@@ -68,7 +66,7 @@ describe('ParseGraphQLSchema', () => {
const graphQLSubscriptions = parseGraphQLSchema.graphQLSubscriptions; const graphQLSubscriptions = parseGraphQLSchema.graphQLSubscriptions;
const newClassObject = new Parse.Object('NewClass'); const newClassObject = new Parse.Object('NewClass');
await newClassObject.save(); await newClassObject.save();
await databaseController.schemaCache.clear(); await parseServer.config.schemaCache.clear();
await new Promise(resolve => setTimeout(resolve, 200)); await new Promise(resolve => setTimeout(resolve, 200));
await parseGraphQLSchema.load(); await parseGraphQLSchema.load();
expect(parseClasses).not.toBe(parseGraphQLSchema.parseClasses); expect(parseClasses).not.toBe(parseGraphQLSchema.parseClasses);
@@ -426,14 +424,14 @@ describe('ParseGraphQLSchema', () => {
log: defaultLogger, log: defaultLogger,
appId, appId,
}); });
await parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLSchema.schemaCache.clear();
const schema1 = await parseGraphQLSchema.load(); const schema1 = await parseGraphQLSchema.load();
const types1 = parseGraphQLSchema.graphQLTypes; const types1 = parseGraphQLSchema.graphQLTypes;
const queries1 = parseGraphQLSchema.graphQLQueries; const queries1 = parseGraphQLSchema.graphQLQueries;
const mutations1 = parseGraphQLSchema.graphQLMutations; const mutations1 = parseGraphQLSchema.graphQLMutations;
const user = new Parse.Object('User'); const user = new Parse.Object('User');
await user.save(); await user.save();
await parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLSchema.schemaCache.clear();
const schema2 = await parseGraphQLSchema.load(); const schema2 = await parseGraphQLSchema.load();
const types2 = parseGraphQLSchema.graphQLTypes; const types2 = parseGraphQLSchema.graphQLTypes;
const queries2 = parseGraphQLSchema.graphQLQueries; const queries2 = parseGraphQLSchema.graphQLQueries;
@@ -456,14 +454,14 @@ describe('ParseGraphQLSchema', () => {
}); });
const car1 = new Parse.Object('Car'); const car1 = new Parse.Object('Car');
await car1.save(); await car1.save();
await parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLSchema.schemaCache.clear();
const schema1 = await parseGraphQLSchema.load(); const schema1 = await parseGraphQLSchema.load();
const types1 = parseGraphQLSchema.graphQLTypes; const types1 = parseGraphQLSchema.graphQLTypes;
const queries1 = parseGraphQLSchema.graphQLQueries; const queries1 = parseGraphQLSchema.graphQLQueries;
const mutations1 = parseGraphQLSchema.graphQLMutations; const mutations1 = parseGraphQLSchema.graphQLMutations;
const car2 = new Parse.Object('car'); const car2 = new Parse.Object('car');
await car2.save(); await car2.save();
await parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLSchema.schemaCache.clear();
const schema2 = await parseGraphQLSchema.load(); const schema2 = await parseGraphQLSchema.load();
const types2 = parseGraphQLSchema.graphQLTypes; const types2 = parseGraphQLSchema.graphQLTypes;
const queries2 = parseGraphQLSchema.graphQLQueries; const queries2 = parseGraphQLSchema.graphQLQueries;
@@ -486,13 +484,13 @@ describe('ParseGraphQLSchema', () => {
}); });
const car = new Parse.Object('Car'); const car = new Parse.Object('Car');
await car.save(); await car.save();
await parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLSchema.schemaCache.clear();
const schema1 = await parseGraphQLSchema.load(); const schema1 = await parseGraphQLSchema.load();
const queries1 = parseGraphQLSchema.graphQLQueries; const queries1 = parseGraphQLSchema.graphQLQueries;
const mutations1 = parseGraphQLSchema.graphQLMutations; const mutations1 = parseGraphQLSchema.graphQLMutations;
const cars = new Parse.Object('cars'); const cars = new Parse.Object('cars');
await cars.save(); await cars.save();
await parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLSchema.schemaCache.clear();
const schema2 = await parseGraphQLSchema.load(); const schema2 = await parseGraphQLSchema.load();
const queries2 = parseGraphQLSchema.graphQLQueries; const queries2 = parseGraphQLSchema.graphQLQueries;
const mutations2 = parseGraphQLSchema.graphQLMutations; const mutations2 = parseGraphQLSchema.graphQLMutations;
@@ -532,7 +530,7 @@ describe('ParseGraphQLSchema', () => {
await data.save(); await data.save();
await parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLSchema.schemaCache.clear();
await parseGraphQLSchema.load(); await parseGraphQLSchema.load();
const queries1 = parseGraphQLSchema.graphQLQueries; const queries1 = parseGraphQLSchema.graphQLQueries;
@@ -569,7 +567,7 @@ describe('ParseGraphQLSchema', () => {
await data.save(); await data.save();
await parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLSchema.schemaCache.clear();
await parseGraphQLSchema.load(); await parseGraphQLSchema.load();
const mutations = parseGraphQLSchema.graphQLMutations; const mutations = parseGraphQLSchema.graphQLMutations;

View File

@@ -538,7 +538,7 @@ describe('ParseGraphQLServer', () => {
const resetGraphQLCache = async () => { const resetGraphQLCache = async () => {
await Promise.all([ await Promise.all([
parseGraphQLServer.parseGraphQLController.cacheController.graphQL.clear(), parseGraphQLServer.parseGraphQLController.cacheController.graphQL.clear(),
parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(), parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(),
]); ]);
}; };
@@ -1092,7 +1092,7 @@ describe('ParseGraphQLServer', () => {
const obj = new Parse.Object('SomeClass'); const obj = new Parse.Object('SomeClass');
await obj.save(); await obj.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const createObjectInputFields = ( const createObjectInputFields = (
await apolloClient.query({ await apolloClient.query({
@@ -1117,7 +1117,7 @@ describe('ParseGraphQLServer', () => {
const obj = new Parse.Object('SomeClass'); const obj = new Parse.Object('SomeClass');
await obj.save(); await obj.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const createObjectPayloadFields = ( const createObjectPayloadFields = (
await apolloClient.query({ await apolloClient.query({
@@ -1142,7 +1142,7 @@ describe('ParseGraphQLServer', () => {
const obj = new Parse.Object('SomeClass'); const obj = new Parse.Object('SomeClass');
await obj.save(); await obj.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const createObjectInputFields = ( const createObjectInputFields = (
await apolloClient.query({ await apolloClient.query({
@@ -1167,7 +1167,7 @@ describe('ParseGraphQLServer', () => {
const obj = new Parse.Object('SomeClass'); const obj = new Parse.Object('SomeClass');
await obj.save(); await obj.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const createObjectPayloadFields = ( const createObjectPayloadFields = (
await apolloClient.query({ await apolloClient.query({
@@ -1192,7 +1192,7 @@ describe('ParseGraphQLServer', () => {
const obj = new Parse.Object('SomeClass'); const obj = new Parse.Object('SomeClass');
await obj.save(); await obj.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const createObjectInputFields = ( const createObjectInputFields = (
await apolloClient.query({ await apolloClient.query({
@@ -1217,7 +1217,7 @@ describe('ParseGraphQLServer', () => {
const obj = new Parse.Object('SomeClass'); const obj = new Parse.Object('SomeClass');
await obj.save(); await obj.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const createObjectPayloadFields = ( const createObjectPayloadFields = (
await apolloClient.query({ await apolloClient.query({
@@ -1339,7 +1339,7 @@ describe('ParseGraphQLServer', () => {
const resetGraphQLCache = async () => { const resetGraphQLCache = async () => {
await Promise.all([ await Promise.all([
parseGraphQLServer.parseGraphQLController.cacheController.graphQL.clear(), parseGraphQLServer.parseGraphQLController.cacheController.graphQL.clear(),
parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(), parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(),
]); ]);
}; };
@@ -3921,7 +3921,7 @@ describe('ParseGraphQLServer', () => {
obj.set('someField', 'someValue'); obj.set('someField', 'someValue');
await obj.save(); await obj.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const result = ( const result = (
await apolloClient.query({ await apolloClient.query({
@@ -3964,7 +3964,7 @@ describe('ParseGraphQLServer', () => {
obj3.set('manyRelations', [obj1, obj2]); obj3.set('manyRelations', [obj1, obj2]);
await obj3.save(); await obj3.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const result = ( const result = (
await apolloClient.query({ await apolloClient.query({
@@ -4039,7 +4039,7 @@ describe('ParseGraphQLServer', () => {
obj1.set('country', obj4); obj1.set('country', obj4);
await obj1.save(); await obj1.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const result = ( const result = (
await apolloClient.query({ await apolloClient.query({
@@ -4130,7 +4130,7 @@ describe('ParseGraphQLServer', () => {
it('should respect level permissions', async () => { it('should respect level permissions', async () => {
await prepareData(); await prepareData();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
async function getObject(className, id, headers) { async function getObject(className, id, headers) {
const alias = className.charAt(0).toLowerCase() + className.slice(1); const alias = className.charAt(0).toLowerCase() + className.slice(1);
@@ -4260,7 +4260,7 @@ describe('ParseGraphQLServer', () => {
it('should support keys argument', async () => { it('should support keys argument', async () => {
await prepareData(); await prepareData();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const result1 = await apolloClient.query({ const result1 = await apolloClient.query({
query: gql` query: gql`
@@ -4310,7 +4310,7 @@ describe('ParseGraphQLServer', () => {
it('should support include argument', async () => { it('should support include argument', async () => {
await prepareData(); await prepareData();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const result1 = await apolloClient.query({ const result1 = await apolloClient.query({
query: gql` query: gql`
@@ -4358,7 +4358,7 @@ describe('ParseGraphQLServer', () => {
it('should respect protectedFields', async done => { it('should respect protectedFields', async done => {
await prepareData(); await prepareData();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const className = 'GraphQLClass'; const className = 'GraphQLClass';
@@ -4439,7 +4439,7 @@ describe('ParseGraphQLServer', () => {
try { try {
await prepareData(); await prepareData();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const databaseAdapter = parseServer.config.databaseController.adapter; const databaseAdapter = parseServer.config.databaseController.adapter;
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
@@ -4486,7 +4486,7 @@ describe('ParseGraphQLServer', () => {
it('should support readPreference argument', async () => { it('should support readPreference argument', async () => {
await prepareData(); await prepareData();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const databaseAdapter = parseServer.config.databaseController.adapter; const databaseAdapter = parseServer.config.databaseController.adapter;
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
@@ -4530,7 +4530,7 @@ describe('ParseGraphQLServer', () => {
it('should support includeReadPreference argument', async () => { it('should support includeReadPreference argument', async () => {
await prepareData(); await prepareData();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const databaseAdapter = parseServer.config.databaseController.adapter; const databaseAdapter = parseServer.config.databaseController.adapter;
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
@@ -4585,7 +4585,7 @@ describe('ParseGraphQLServer', () => {
obj2.set('someField', 'someValue1'); obj2.set('someField', 'someValue1');
await obj2.save(); await obj2.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const result = await apolloClient.query({ const result = await apolloClient.query({
query: gql` query: gql`
@@ -4618,7 +4618,7 @@ describe('ParseGraphQLServer', () => {
it('should respect level permissions', async () => { it('should respect level permissions', async () => {
await prepareData(); await prepareData();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
async function findObjects(className, headers) { async function findObjects(className, headers) {
const graphqlClassName = pluralize( const graphqlClassName = pluralize(
@@ -4724,7 +4724,7 @@ describe('ParseGraphQLServer', () => {
it('should support where argument using class specific query', async () => { it('should support where argument using class specific query', async () => {
await prepareData(); await prepareData();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const result = await apolloClient.query({ const result = await apolloClient.query({
query: gql` query: gql`
@@ -4776,7 +4776,7 @@ describe('ParseGraphQLServer', () => {
it('should support in pointer operator using class specific query', async () => { it('should support in pointer operator using class specific query', async () => {
await prepareData(); await prepareData();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const result = await apolloClient.query({ const result = await apolloClient.query({
query: gql` query: gql`
@@ -4816,7 +4816,7 @@ describe('ParseGraphQLServer', () => {
it('should support OR operation', async () => { it('should support OR operation', async () => {
await prepareData(); await prepareData();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const result = await apolloClient.query({ const result = await apolloClient.query({
query: gql` query: gql`
@@ -4856,7 +4856,7 @@ describe('ParseGraphQLServer', () => {
obj.set('field2', 'It rocks!'); obj.set('field2', 'It rocks!');
await obj.save(); await obj.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const result = await apolloClient.query({ const result = await apolloClient.query({
query: gql` query: gql`
@@ -4914,7 +4914,7 @@ describe('ParseGraphQLServer', () => {
city2.set('name', 'city2'); city2.set('name', 'city2');
await city2.save(); await city2.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const { const {
data: { data: {
@@ -4970,7 +4970,7 @@ describe('ParseGraphQLServer', () => {
} }
await Promise.all(promises); await Promise.all(promises);
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const result = await apolloClient.query({ const result = await apolloClient.query({
query: gql` query: gql`
@@ -5024,7 +5024,7 @@ describe('ParseGraphQLServer', () => {
} }
await Promise.all(promises); await Promise.all(promises);
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const find = async ({ skip, after, first, before, last } = {}) => { const find = async ({ skip, after, first, before, last } = {}) => {
return await apolloClient.query({ return await apolloClient.query({
@@ -5152,7 +5152,7 @@ describe('ParseGraphQLServer', () => {
it('should support count', async () => { it('should support count', async () => {
await prepareData(); await prepareData();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const where = { const where = {
someField: { someField: {
@@ -5207,7 +5207,7 @@ describe('ParseGraphQLServer', () => {
it('should only count', async () => { it('should only count', async () => {
await prepareData(); await prepareData();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const where = { const where = {
someField: { someField: {
@@ -5265,7 +5265,7 @@ describe('ParseGraphQLServer', () => {
} }
await Promise.all(promises); await Promise.all(promises);
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const result = await apolloClient.query({ const result = await apolloClient.query({
query: gql` query: gql`
@@ -5297,7 +5297,7 @@ describe('ParseGraphQLServer', () => {
it('should support keys argument', async () => { it('should support keys argument', async () => {
await prepareData(); await prepareData();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const result1 = await apolloClient.query({ const result1 = await apolloClient.query({
query: gql` query: gql`
@@ -5359,7 +5359,7 @@ describe('ParseGraphQLServer', () => {
it('should support include argument', async () => { it('should support include argument', async () => {
await prepareData(); await prepareData();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const where = { const where = {
id: { id: {
@@ -5422,7 +5422,7 @@ describe('ParseGraphQLServer', () => {
it('should read from primary by default', async () => { it('should read from primary by default', async () => {
await prepareData(); await prepareData();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const databaseAdapter = parseServer.config.databaseController.adapter; const databaseAdapter = parseServer.config.databaseController.adapter;
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
@@ -5467,7 +5467,7 @@ describe('ParseGraphQLServer', () => {
it('should support readPreference argument', async () => { it('should support readPreference argument', async () => {
await prepareData(); await prepareData();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const databaseAdapter = parseServer.config.databaseController.adapter; const databaseAdapter = parseServer.config.databaseController.adapter;
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
@@ -5512,7 +5512,7 @@ describe('ParseGraphQLServer', () => {
it('should support includeReadPreference argument', async () => { it('should support includeReadPreference argument', async () => {
await prepareData(); await prepareData();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const databaseAdapter = parseServer.config.databaseController.adapter; const databaseAdapter = parseServer.config.databaseController.adapter;
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
@@ -5560,7 +5560,7 @@ describe('ParseGraphQLServer', () => {
try { try {
await prepareData(); await prepareData();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const databaseAdapter = parseServer.config.databaseController.adapter; const databaseAdapter = parseServer.config.databaseController.adapter;
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
@@ -5714,7 +5714,7 @@ describe('ParseGraphQLServer', () => {
customerSchema.addString('someField'); customerSchema.addString('someField');
await customerSchema.save(); await customerSchema.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const result = await apolloClient.mutate({ const result = await apolloClient.mutate({
mutation: gql` mutation: gql`
@@ -5757,7 +5757,7 @@ describe('ParseGraphQLServer', () => {
it('should respect level permissions', async () => { it('should respect level permissions', async () => {
await prepareData(); await prepareData();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
async function createObject(className, headers) { async function createObject(className, headers) {
const getClassName = className.charAt(0).toLowerCase() + className.slice(1); const getClassName = className.charAt(0).toLowerCase() + className.slice(1);
@@ -5837,7 +5837,7 @@ describe('ParseGraphQLServer', () => {
obj.set('someField2', 'someField2Value1'); obj.set('someField2', 'someField2Value1');
await obj.save(); await obj.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const result = await apolloClient.mutate({ const result = await apolloClient.mutate({
mutation: gql` mutation: gql`
@@ -5880,7 +5880,7 @@ describe('ParseGraphQLServer', () => {
obj.set('someField2', 'someField2Value1'); obj.set('someField2', 'someField2Value1');
await obj.save(); await obj.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const result = await apolloClient.mutate({ const result = await apolloClient.mutate({
mutation: gql` mutation: gql`
@@ -5912,7 +5912,7 @@ describe('ParseGraphQLServer', () => {
it('should respect level permissions', async () => { it('should respect level permissions', async () => {
await prepareData(); await prepareData();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
async function updateObject(className, id, fields, headers) { async function updateObject(className, id, fields, headers) {
return await apolloClient.mutate({ return await apolloClient.mutate({
@@ -6107,7 +6107,7 @@ describe('ParseGraphQLServer', () => {
it('should respect level permissions with specific class mutation', async () => { it('should respect level permissions with specific class mutation', async () => {
await prepareData(); await prepareData();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
function updateObject(className, id, fields, headers) { function updateObject(className, id, fields, headers) {
const mutationName = className.charAt(0).toLowerCase() + className.slice(1); const mutationName = className.charAt(0).toLowerCase() + className.slice(1);
@@ -6327,7 +6327,7 @@ describe('ParseGraphQLServer', () => {
obj.set('someField2', 'someField2Value1'); obj.set('someField2', 'someField2Value1');
await obj.save(); await obj.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const result = await apolloClient.mutate({ const result = await apolloClient.mutate({
mutation: gql` mutation: gql`
@@ -6364,7 +6364,7 @@ describe('ParseGraphQLServer', () => {
it('should respect level permissions', async () => { it('should respect level permissions', async () => {
await prepareData(); await prepareData();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
function deleteObject(className, id, headers) { function deleteObject(className, id, headers) {
const mutationName = className.charAt(0).toLowerCase() + className.slice(1); const mutationName = className.charAt(0).toLowerCase() + className.slice(1);
@@ -6454,7 +6454,7 @@ describe('ParseGraphQLServer', () => {
it('should respect level permissions with specific class mutation', async () => { it('should respect level permissions with specific class mutation', async () => {
await prepareData(); await prepareData();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
function deleteObject(className, id, headers) { function deleteObject(className, id, headers) {
const mutationName = className.charAt(0).toLowerCase() + className.slice(1); const mutationName = className.charAt(0).toLowerCase() + className.slice(1);
@@ -6666,7 +6666,7 @@ describe('ParseGraphQLServer', () => {
user.set('userFoo', foo); user.set('userFoo', foo);
await user.signUp(); await user.signUp();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const session = await Parse.Session.current(); const session = await Parse.Session.current();
const result = await apolloClient.query({ const result = await apolloClient.query({
@@ -6717,7 +6717,7 @@ describe('ParseGraphQLServer', () => {
user.set('userFoo', foo); user.set('userFoo', foo);
await user.signUp(); await user.signUp();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const session = await Parse.Session.current(); const session = await Parse.Session.current();
const result = await apolloClient.query({ const result = await apolloClient.query({
@@ -6758,7 +6758,7 @@ describe('ParseGraphQLServer', () => {
userSchema.addPointer('aPointer', '_User'); userSchema.addPointer('aPointer', '_User');
await userSchema.update(); await userSchema.update();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const result = await apolloClient.mutate({ const result = await apolloClient.mutate({
mutation: gql` mutation: gql`
mutation SignUp($input: SignUpInput!) { mutation SignUp($input: SignUpInput!) {
@@ -6819,7 +6819,7 @@ describe('ParseGraphQLServer', () => {
userSchema.addString('someField'); userSchema.addString('someField');
userSchema.addPointer('aPointer', '_User'); userSchema.addPointer('aPointer', '_User');
await userSchema.update(); await userSchema.update();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const result = await apolloClient.mutate({ const result = await apolloClient.mutate({
mutation: gql` mutation: gql`
mutation LogInWith($input: LogInWithInput!) { mutation LogInWith($input: LogInWithInput!) {
@@ -6877,7 +6877,7 @@ describe('ParseGraphQLServer', () => {
user.set('someField', 'someValue'); user.set('someField', 'someValue');
await user.signUp(); await user.signUp();
await Parse.User.logOut(); await Parse.User.logOut();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const result = await apolloClient.mutate({ const result = await apolloClient.mutate({
mutation: gql` mutation: gql`
mutation LogInUser($input: LogInInput!) { mutation LogInUser($input: LogInInput!) {
@@ -7120,7 +7120,7 @@ describe('ParseGraphQLServer', () => {
const car = new Parse.Object('Car'); const car = new Parse.Object('Car');
await car.save(); await car.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
try { try {
await apolloClient.query({ await apolloClient.query({
@@ -7418,7 +7418,7 @@ describe('ParseGraphQLServer', () => {
}, },
}); });
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const schema = await new Parse.Schema('SomeClass').get(); const schema = await new Parse.Schema('SomeClass').get();
expect(schema.fields.someField.type).toEqual('String'); expect(schema.fields.someField.type).toEqual('String');
@@ -7493,7 +7493,7 @@ describe('ParseGraphQLServer', () => {
}, },
}); });
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const createResult = await apolloClient.mutate({ const createResult = await apolloClient.mutate({
mutation: gql` mutation: gql`
@@ -7568,7 +7568,7 @@ describe('ParseGraphQLServer', () => {
}, },
}); });
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const schema = await new Parse.Schema('SomeClass').get(); const schema = await new Parse.Schema('SomeClass').get();
expect(schema.fields.someField.type).toEqual('Number'); expect(schema.fields.someField.type).toEqual('Number');
@@ -7644,7 +7644,7 @@ describe('ParseGraphQLServer', () => {
}, },
}); });
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const schema = await new Parse.Schema('SomeClass').get(); const schema = await new Parse.Schema('SomeClass').get();
expect(schema.fields.someFieldTrue.type).toEqual('Boolean'); expect(schema.fields.someFieldTrue.type).toEqual('Boolean');
@@ -7734,7 +7734,7 @@ describe('ParseGraphQLServer', () => {
}, },
}); });
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const schema = await new Parse.Schema('SomeClass').get(); const schema = await new Parse.Schema('SomeClass').get();
expect(schema.fields.someField.type).toEqual('Date'); expect(schema.fields.someField.type).toEqual('Date');
@@ -7827,7 +7827,7 @@ describe('ParseGraphQLServer', () => {
const role2 = new Parse.Role('aRole2', roleACL); const role2 = new Parse.Role('aRole2', roleACL);
await role2.save(); await role2.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const gqlUser = ( const gqlUser = (
await apolloClient.query({ await apolloClient.query({
@@ -8013,7 +8013,7 @@ describe('ParseGraphQLServer', () => {
company2.set('name', 'imACompany2'); company2.set('name', 'imACompany2');
await company2.save(); await company2.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const { const {
data: { data: {
@@ -8058,7 +8058,7 @@ describe('ParseGraphQLServer', () => {
country.set('company', company); country.set('company', company);
await country.save(); await country.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const { const {
data: { data: {
@@ -8109,7 +8109,7 @@ describe('ParseGraphQLServer', () => {
company2.set('name', 'imACompany2'); company2.set('name', 'imACompany2');
await company2.save(); await company2.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const { const {
data: { data: {
@@ -8154,7 +8154,7 @@ describe('ParseGraphQLServer', () => {
country.set('company', company); country.set('company', company);
await country.save(); await country.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const { const {
data: { data: {
@@ -8201,7 +8201,7 @@ describe('ParseGraphQLServer', () => {
country.relation('companies').add(company); country.relation('companies').add(company);
await country.save(); await country.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const { const {
data: { data: {
@@ -8269,7 +8269,7 @@ describe('ParseGraphQLServer', () => {
country.relation('companies').add(company); country.relation('companies').add(company);
await country.save(); await country.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const { const {
data: { data: {
@@ -8362,7 +8362,7 @@ describe('ParseGraphQLServer', () => {
country.relation('companies').add(company1); country.relation('companies').add(company1);
await country.save(); await country.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const { const {
data: { data: {
@@ -8421,7 +8421,7 @@ describe('ParseGraphQLServer', () => {
country.relation('companies').add(company); country.relation('companies').add(company);
await country.save(); await country.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const { const {
data: { data: {
@@ -8489,7 +8489,7 @@ describe('ParseGraphQLServer', () => {
country.relation('companies').add([company1, company2]); country.relation('companies').add([company1, company2]);
await country.save(); await country.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
// Without where // Without where
const { const {
@@ -8589,7 +8589,7 @@ describe('ParseGraphQLServer', () => {
country3.set('president', president); country3.set('president', president);
await country3.save(); await country3.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
let { let {
data: { data: {
@@ -8856,7 +8856,7 @@ describe('ParseGraphQLServer', () => {
}, },
}); });
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const body2 = new FormData(); const body2 = new FormData();
body2.append( body2.append(
@@ -9057,7 +9057,7 @@ describe('ParseGraphQLServer', () => {
}, },
}, },
}); });
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const schema = await new Parse.Schema('SomeClass').get(); const schema = await new Parse.Schema('SomeClass').get();
expect(schema.fields.someObjectField.type).toEqual('Object'); expect(schema.fields.someObjectField.type).toEqual('Object');
@@ -9157,7 +9157,7 @@ describe('ParseGraphQLServer', () => {
}, },
}); });
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const createResult = await apolloClient.mutate({ const createResult = await apolloClient.mutate({
mutation: gql` mutation: gql`
@@ -9274,7 +9274,7 @@ describe('ParseGraphQLServer', () => {
}, },
}); });
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const schema = await new Parse.Schema('SomeClass').get(); const schema = await new Parse.Schema('SomeClass').get();
expect(schema.fields.someArrayField.type).toEqual('Array'); expect(schema.fields.someArrayField.type).toEqual('Array');
@@ -9342,7 +9342,7 @@ describe('ParseGraphQLServer', () => {
const obj = new Parse.Object('SomeClass'); const obj = new Parse.Object('SomeClass');
await obj.save(); await obj.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const getResult = await apolloClient.query({ const getResult = await apolloClient.query({
query: gql` query: gql`
@@ -9391,7 +9391,7 @@ describe('ParseGraphQLServer', () => {
}, },
}); });
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const createResult = await apolloClient.mutate({ const createResult = await apolloClient.mutate({
mutation: gql` mutation: gql`
@@ -9485,7 +9485,7 @@ describe('ParseGraphQLServer', () => {
}, },
}); });
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const schema = await new Parse.Schema('SomeClass').get(); const schema = await new Parse.Schema('SomeClass').get();
expect(schema.fields.someField.type).toEqual('Bytes'); expect(schema.fields.someField.type).toEqual('Bytes');
@@ -9576,7 +9576,7 @@ describe('ParseGraphQLServer', () => {
}, },
}); });
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const schema = await new Parse.Schema('SomeClass').get(); const schema = await new Parse.Schema('SomeClass').get();
expect(schema.fields.someField.type).toEqual('GeoPoint'); expect(schema.fields.someField.type).toEqual('GeoPoint');
@@ -9735,7 +9735,7 @@ describe('ParseGraphQLServer', () => {
}, },
}); });
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const schema = await new Parse.Schema('SomeClass').get(); const schema = await new Parse.Schema('SomeClass').get();
expect(schema.fields.somePolygonField.type).toEqual('Polygon'); expect(schema.fields.somePolygonField.type).toEqual('Polygon');
@@ -9830,7 +9830,7 @@ describe('ParseGraphQLServer', () => {
}); });
await someClass.save(); await someClass.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const schema = await new Parse.Schema('SomeClass').get(); const schema = await new Parse.Schema('SomeClass').get();
expect(schema.fields.someField.type).toEqual('Bytes'); expect(schema.fields.someField.type).toEqual('Bytes');
@@ -9930,7 +9930,7 @@ describe('ParseGraphQLServer', () => {
user.setPassword('user1'); user.setPassword('user1');
await user.signUp(); await user.signUp();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const getResult = await apolloClient.query({ const getResult = await apolloClient.query({
query: gql` query: gql`
@@ -9954,7 +9954,7 @@ describe('ParseGraphQLServer', () => {
deviceType: 'foo', deviceType: 'foo',
}); });
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const getResult = await apolloClient.query({ const getResult = await apolloClient.query({
query: gql` query: gql`
@@ -9978,7 +9978,7 @@ describe('ParseGraphQLServer', () => {
const role = new Parse.Role('MyRole', roleACL); const role = new Parse.Role('MyRole', roleACL);
await role.save(); await role.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const getResult = await apolloClient.query({ const getResult = await apolloClient.query({
query: gql` query: gql`
@@ -10002,7 +10002,7 @@ describe('ParseGraphQLServer', () => {
user.setPassword('user1'); user.setPassword('user1');
await user.signUp(); await user.signUp();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const session = await Parse.Session.current(); const session = await Parse.Session.current();
const getResult = await apolloClient.query({ const getResult = await apolloClient.query({
@@ -10041,7 +10041,7 @@ describe('ParseGraphQLServer', () => {
{ useMasterKey: true } { useMasterKey: true }
); );
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const getResult = await apolloClient.query({ const getResult = await apolloClient.query({
query: gql` query: gql`
@@ -10170,7 +10170,7 @@ describe('ParseGraphQLServer', () => {
await Promise.all([ await Promise.all([
parseGraphQLServer.parseGraphQLController.cacheController.graphQL.clear(), parseGraphQLServer.parseGraphQLController.cacheController.graphQL.clear(),
parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(), parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(),
]); ]);
await expectAsync( await expectAsync(
@@ -10311,7 +10311,7 @@ describe('ParseGraphQLServer', () => {
it('can resolve a custom query with auto type return', async () => { it('can resolve a custom query with auto type return', async () => {
const obj = new Parse.Object('SomeClass'); const obj = new Parse.Object('SomeClass');
await obj.save({ name: 'aname', type: 'robot' }); await obj.save({ name: 'aname', type: 'robot' });
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const result = await apolloClient.query({ const result = await apolloClient.query({
variables: { id: obj.id }, variables: { id: obj.id },
query: gql` query: gql`
@@ -10334,7 +10334,7 @@ describe('ParseGraphQLServer', () => {
it('can resolve a custom extend type', async () => { it('can resolve a custom extend type', async () => {
const obj = new Parse.Object('SomeClass'); const obj = new Parse.Object('SomeClass');
await obj.save({ name: 'aname', type: 'robot' }); await obj.save({ name: 'aname', type: 'robot' });
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const result = await apolloClient.query({ const result = await apolloClient.query({
variables: { id: obj.id }, variables: { id: obj.id },
query: gql` query: gql`

View File

@@ -24,7 +24,6 @@ describe_only_db('mongo')('Parse.Query hint', () => {
}); });
afterEach(async () => { afterEach(async () => {
await config.database.schemaCache.clear();
await TestUtils.destroyAllDataPermanently(false); await TestUtils.destroyAllDataPermanently(false);
}); });

View File

@@ -247,6 +247,7 @@ describe('Parse.User testing', () => {
await adapter.connect(); await adapter.connect();
await adapter.database.dropDatabase(); await adapter.database.dropDatabase();
delete adapter.connectionPromise; delete adapter.connectionPromise;
Config.get(Parse.applicationId).schemaCache.clear();
const user = new Parse.User(); const user = new Parse.User();
await user.signUp({ await user.signUp({

View File

@@ -3,7 +3,7 @@ const Config = require('../lib/Config');
describe('Pointer Permissions', () => { describe('Pointer Permissions', () => {
beforeEach(() => { beforeEach(() => {
Config.get(Parse.applicationId).database.schemaCache.clear(); Config.get(Parse.applicationId).schemaCache.clear();
}); });
describe('using single user-pointers', () => { describe('using single user-pointers', () => {
@@ -2020,7 +2020,7 @@ describe('Pointer Permissions', () => {
let obj2; let obj2;
async function initialize() { async function initialize() {
await Config.get(Parse.applicationId).database.schemaCache.clear(); await Config.get(Parse.applicationId).schemaCache.clear();
[user1, user2] = await Promise.all([createUser('user1'), createUser('user2')]); [user1, user2] = await Promise.all([createUser('user1'), createUser('user2')]);
@@ -2442,7 +2442,7 @@ describe('Pointer Permissions', () => {
let objNobody; let objNobody;
async function initialize() { async function initialize() {
await Config.get(Parse.applicationId).database.schemaCache.clear(); await Config.get(Parse.applicationId).schemaCache.clear();
[user1, user2, user3] = await Promise.all([ [user1, user2, user3] = await Promise.all([
createUser('user1'), createUser('user1'),
@@ -2919,7 +2919,7 @@ describe('Pointer Permissions', () => {
let obj2; let obj2;
async function initialize() { async function initialize() {
await Config.get(Parse.applicationId).database.schemaCache.clear(); await Config.get(Parse.applicationId).schemaCache.clear();
[user1, user2] = await Promise.all([createUser('user1'), createUser('user2')]); [user1, user2] = await Promise.all([createUser('user1'), createUser('user2')]);
@@ -3033,7 +3033,7 @@ describe('Pointer Permissions', () => {
* Clear cache, create user and object, login user * Clear cache, create user and object, login user
*/ */
async function initialize() { async function initialize() {
await Config.get(Parse.applicationId).database.schemaCache.clear(); await Config.get(Parse.applicationId).schemaCache.clear();
user1 = await createUser('user1'); user1 = await createUser('user1');
user1 = await logIn(user1); user1 = await logIn(user1);

View File

@@ -26,6 +26,8 @@ describe_only_db('postgres')('PostgresStorageAdapter', () => {
it('schemaUpgrade, upgrade the database schema when schema changes', async done => { it('schemaUpgrade, upgrade the database schema when schema changes', async done => {
await adapter.deleteAllClasses(); await adapter.deleteAllClasses();
const config = Config.get('test');
config.schemaCache.clear();
await adapter.performInitialization({ VolatileClassesSchemas: [] }); await adapter.performInitialization({ VolatileClassesSchemas: [] });
const client = adapter._client; const client = adapter._client;
const className = '_PushStatus'; const className = '_PushStatus';
@@ -232,13 +234,19 @@ describe_only_db('postgres')('PostgresStorageAdapter', () => {
}); });
it('should use index for caseInsensitive query', async () => { it('should use index for caseInsensitive query', async () => {
await adapter.deleteAllClasses();
const config = Config.get('test');
config.schemaCache.clear();
await adapter.performInitialization({ VolatileClassesSchemas: [] });
const database = Config.get(Parse.applicationId).database;
await database.loadSchema({ clearCache: true });
const tableName = '_User'; const tableName = '_User';
const user = new Parse.User(); const user = new Parse.User();
user.set('username', 'Elmer'); user.set('username', 'Elmer');
user.set('password', 'Fudd'); user.set('password', 'Fudd');
await user.signUp(); await user.signUp();
const database = Config.get(Parse.applicationId).database;
//Postgres won't take advantage of the index until it has a lot of records because sequential is faster for small db's //Postgres won't take advantage of the index until it has a lot of records because sequential is faster for small db's
const client = adapter._client; const client = adapter._client;
@@ -287,12 +295,19 @@ describe_only_db('postgres')('PostgresStorageAdapter', () => {
}); });
it('should use index for caseInsensitive query using default indexname', async () => { it('should use index for caseInsensitive query using default indexname', async () => {
await adapter.deleteAllClasses();
const config = Config.get('test');
config.schemaCache.clear();
await adapter.performInitialization({ VolatileClassesSchemas: [] });
const database = Config.get(Parse.applicationId).database;
await database.loadSchema({ clearCache: true });
const tableName = '_User'; const tableName = '_User';
const user = new Parse.User(); const user = new Parse.User();
user.set('username', 'Tweety'); user.set('username', 'Tweety');
user.set('password', 'Bird'); user.set('password', 'Bird');
await user.signUp(); await user.signUp();
const database = Config.get(Parse.applicationId).database;
const fieldToSearch = 'username'; const fieldToSearch = 'username';
//Create index before data is inserted //Create index before data is inserted
const schema = await new Parse.Schema('_User').get(); const schema = await new Parse.Schema('_User').get();
@@ -375,6 +390,45 @@ describe_only_db('postgres')('PostgresStorageAdapter', () => {
}); });
}); });
}); });
it('should watch _SCHEMA changes', async () => {
const enableSchemaHooks = true;
await reconfigureServer({
databaseAdapter: undefined,
databaseURI,
collectionPrefix: '',
databaseOptions: {
enableSchemaHooks,
},
});
const { database } = Config.get(Parse.applicationId);
const { adapter } = database;
expect(adapter.enableSchemaHooks).toBe(enableSchemaHooks);
spyOn(adapter, '_onchange');
enableSchemaHooks;
const otherInstance = new PostgresStorageAdapter({
uri: databaseURI,
collectionPrefix: '',
databaseOptions: { enableSchemaHooks },
});
expect(otherInstance.enableSchemaHooks).toBe(enableSchemaHooks);
otherInstance._listenToSchema();
await otherInstance.createClass('Stuff', {
className: 'Stuff',
fields: {
objectId: { type: 'String' },
createdAt: { type: 'Date' },
updatedAt: { type: 'Date' },
_rperm: { type: 'Array' },
_wperm: { type: 'Array' },
},
classLevelPermissions: undefined,
});
await new Promise(resolve => setTimeout(resolve, 500));
expect(adapter._onchange).toHaveBeenCalled();
});
}); });
describe_only_db('postgres')('PostgresStorageAdapter shutdown', () => { describe_only_db('postgres')('PostgresStorageAdapter shutdown', () => {

View File

@@ -135,7 +135,7 @@ describe('ProtectedFields', function () {
describe('using the pointer-permission variant', () => { describe('using the pointer-permission variant', () => {
let user1, user2; let user1, user2;
beforeEach(async () => { beforeEach(async () => {
Config.get(Parse.applicationId).database.schemaCache.clear(); Config.get(Parse.applicationId).schemaCache.clear();
user1 = await Parse.User.signUp('user1', 'password'); user1 = await Parse.User.signUp('user1', 'password');
user2 = await Parse.User.signUp('user2', 'password'); user2 = await Parse.User.signUp('user2', 'password');
await Parse.User.logOut(); await Parse.User.logOut();
@@ -752,7 +752,7 @@ describe('ProtectedFields', function () {
let object; let object;
async function initialize() { async function initialize() {
await Config.get(Parse.applicationId).database.schemaCache.clear(); await Config.get(Parse.applicationId).schemaCache.clear();
object = new Parse.Object(className); object = new Parse.Object(className);
@@ -815,7 +815,7 @@ describe('ProtectedFields', function () {
let obj1; let obj1;
async function initialize() { async function initialize() {
await Config.get(Parse.applicationId).database.schemaCache.clear(); await Config.get(Parse.applicationId).schemaCache.clear();
obj1 = new Parse.Object(className); obj1 = new Parse.Object(className);
@@ -924,7 +924,7 @@ describe('ProtectedFields', function () {
let obj2; let obj2;
async function initialize() { async function initialize() {
await Config.get(Parse.applicationId).database.schemaCache.clear(); await Config.get(Parse.applicationId).schemaCache.clear();
await Parse.User.logOut(); await Parse.User.logOut();
@@ -1125,7 +1125,7 @@ describe('ProtectedFields', function () {
let obj2; let obj2;
async function initialize() { async function initialize() {
await Config.get(Parse.applicationId).database.schemaCache.clear(); await Config.get(Parse.applicationId).schemaCache.clear();
[user1, user2] = await Promise.all([createUser('user1'), createUser('user2')]); [user1, user2] = await Promise.all([createUser('user1'), createUser('user2')]);
@@ -1477,7 +1477,7 @@ describe('ProtectedFields', function () {
* Clear cache, create user and object, login user and setup rest headers with token * Clear cache, create user and object, login user and setup rest headers with token
*/ */
async function initialize() { async function initialize() {
await Config.get(Parse.applicationId).database.schemaCache.clear(); await Config.get(Parse.applicationId).schemaCache.clear();
user1 = await createUser('user1'); user1 = await createUser('user1');
user1 = await logIn(user1); user1 = await logIn(user1);

View File

@@ -7,7 +7,7 @@ const Config = require('../lib/Config');
function waitForReplication() { function waitForReplication() {
return new Promise(function (resolve) { return new Promise(function (resolve) {
setTimeout(resolve, 300); setTimeout(resolve, 1000);
}); });
} }

View File

@@ -1,5 +1,4 @@
const RedisCacheAdapter = require('../lib/Adapters/Cache/RedisCacheAdapter').default; const RedisCacheAdapter = require('../lib/Adapters/Cache/RedisCacheAdapter').default;
const Config = require('../lib/Config');
function wait(sleep) { function wait(sleep) {
return new Promise(function (resolve) { return new Promise(function (resolve) {
@@ -168,356 +167,3 @@ describe_only(() => {
.then(done); .then(done);
}); });
}); });
describe_only(() => {
return process.env.PARSE_SERVER_TEST_CACHE === 'redis';
})('Redis Performance', function () {
let cacheAdapter;
let getSpy;
let putSpy;
let delSpy;
beforeEach(async () => {
cacheAdapter = new RedisCacheAdapter();
await reconfigureServer({
cacheAdapter,
});
await cacheAdapter.clear();
getSpy = spyOn(cacheAdapter, 'get').and.callThrough();
putSpy = spyOn(cacheAdapter, 'put').and.callThrough();
delSpy = spyOn(cacheAdapter, 'del').and.callThrough();
});
it('test new object', async () => {
const object = new TestObject();
object.set('foo', 'bar');
await object.save();
expect(getSpy.calls.count()).toBe(3);
expect(putSpy.calls.count()).toBe(3);
expect(delSpy.calls.count()).toBe(1);
const keys = await cacheAdapter.getAllKeys();
expect(keys.length).toBe(0);
});
it('test new object multiple fields', async () => {
const container = new Container({
dateField: new Date(),
arrayField: [],
numberField: 1,
stringField: 'hello',
booleanField: true,
});
await container.save();
expect(getSpy.calls.count()).toBe(3);
expect(putSpy.calls.count()).toBe(3);
expect(delSpy.calls.count()).toBe(1);
const keys = await cacheAdapter.getAllKeys();
expect(keys.length).toBe(0);
});
it('test update existing fields', async () => {
const object = new TestObject();
object.set('foo', 'bar');
await object.save();
getSpy.calls.reset();
putSpy.calls.reset();
object.set('foo', 'barz');
await object.save();
expect(getSpy.calls.count()).toBe(3);
expect(putSpy.calls.count()).toBe(1);
expect(delSpy.calls.count()).toBe(2);
const keys = await cacheAdapter.getAllKeys();
expect(keys.length).toBe(0);
});
it('test saveAll / destroyAll', async () => {
const object = new TestObject();
await object.save();
getSpy.calls.reset();
putSpy.calls.reset();
const objects = [];
for (let i = 0; i < 10; i++) {
const object = new TestObject();
object.set('number', i);
objects.push(object);
}
await Parse.Object.saveAll(objects);
expect(getSpy.calls.count()).toBe(21);
expect(putSpy.calls.count()).toBe(11);
getSpy.calls.reset();
putSpy.calls.reset();
await Parse.Object.destroyAll(objects);
expect(getSpy.calls.count()).toBe(11);
expect(putSpy.calls.count()).toBe(1);
expect(delSpy.calls.count()).toBe(3);
const keys = await cacheAdapter.getAllKeys();
expect(keys.length).toBe(0);
});
it('test saveAll / destroyAll batch', async () => {
const object = new TestObject();
await object.save();
getSpy.calls.reset();
putSpy.calls.reset();
const objects = [];
for (let i = 0; i < 10; i++) {
const object = new TestObject();
object.set('number', i);
objects.push(object);
}
await Parse.Object.saveAll(objects, { batchSize: 5 });
expect(getSpy.calls.count()).toBe(22);
expect(putSpy.calls.count()).toBe(7);
getSpy.calls.reset();
putSpy.calls.reset();
await Parse.Object.destroyAll(objects, { batchSize: 5 });
expect(getSpy.calls.count()).toBe(12);
expect(putSpy.calls.count()).toBe(2);
expect(delSpy.calls.count()).toBe(5);
const keys = await cacheAdapter.getAllKeys();
expect(keys.length).toBe(0);
});
it('test add new field to existing object', async () => {
const object = new TestObject();
object.set('foo', 'bar');
await object.save();
getSpy.calls.reset();
putSpy.calls.reset();
object.set('new', 'barz');
await object.save();
expect(getSpy.calls.count()).toBe(3);
expect(putSpy.calls.count()).toBe(2);
expect(delSpy.calls.count()).toBe(2);
const keys = await cacheAdapter.getAllKeys();
expect(keys.length).toBe(0);
});
it('test add multiple fields to existing object', async () => {
const object = new TestObject();
object.set('foo', 'bar');
await object.save();
getSpy.calls.reset();
putSpy.calls.reset();
object.set({
dateField: new Date(),
arrayField: [],
numberField: 1,
stringField: 'hello',
booleanField: true,
});
await object.save();
expect(getSpy.calls.count()).toBe(3);
expect(putSpy.calls.count()).toBe(2);
expect(delSpy.calls.count()).toBe(2);
const keys = await cacheAdapter.getAllKeys();
expect(keys.length).toBe(0);
});
it('test user', async () => {
const user = new Parse.User();
user.setUsername('testing');
user.setPassword('testing');
await user.signUp();
expect(getSpy.calls.count()).toBe(8);
expect(putSpy.calls.count()).toBe(2);
expect(delSpy.calls.count()).toBe(1);
const keys = await cacheAdapter.getAllKeys();
expect(keys.length).toBe(0);
});
it('test allowClientCreation false', async () => {
const object = new TestObject();
await object.save();
await reconfigureServer({
cacheAdapter,
allowClientClassCreation: false,
});
await cacheAdapter.clear();
getSpy.calls.reset();
putSpy.calls.reset();
delSpy.calls.reset();
object.set('foo', 'bar');
await object.save();
expect(getSpy.calls.count()).toBe(4);
expect(putSpy.calls.count()).toBe(2);
getSpy.calls.reset();
putSpy.calls.reset();
const query = new Parse.Query(TestObject);
await query.get(object.id);
expect(getSpy.calls.count()).toBe(3);
expect(putSpy.calls.count()).toBe(1);
expect(delSpy.calls.count()).toBe(2);
const keys = await cacheAdapter.getAllKeys();
expect(keys.length).toBe(0);
});
it('test query', async () => {
const object = new TestObject();
object.set('foo', 'bar');
await object.save();
getSpy.calls.reset();
putSpy.calls.reset();
delSpy.calls.reset();
const query = new Parse.Query(TestObject);
await query.get(object.id);
expect(getSpy.calls.count()).toBe(2);
expect(putSpy.calls.count()).toBe(1);
expect(delSpy.calls.count()).toBe(1);
const keys = await cacheAdapter.getAllKeys();
expect(keys.length).toBe(0);
});
it('test query include', async () => {
const child = new TestObject();
await child.save();
const object = new TestObject();
object.set('child', child);
await object.save();
getSpy.calls.reset();
putSpy.calls.reset();
const query = new Parse.Query(TestObject);
query.include('child');
await query.get(object.id);
expect(getSpy.calls.count()).toBe(4);
expect(putSpy.calls.count()).toBe(1);
expect(delSpy.calls.count()).toBe(3);
const keys = await cacheAdapter.getAllKeys();
expect(keys.length).toBe(0);
});
it('query relation without schema', async () => {
const child = new Parse.Object('ChildObject');
await child.save();
const parent = new Parse.Object('ParentObject');
const relation = parent.relation('child');
relation.add(child);
await parent.save();
getSpy.calls.reset();
putSpy.calls.reset();
const objects = await relation.query().find();
expect(objects.length).toBe(1);
expect(objects[0].id).toBe(child.id);
expect(getSpy.calls.count()).toBe(2);
expect(putSpy.calls.count()).toBe(1);
expect(delSpy.calls.count()).toBe(3);
const keys = await cacheAdapter.getAllKeys();
expect(keys.length).toBe(0);
});
it('test delete object', async () => {
const object = new TestObject();
object.set('foo', 'bar');
await object.save();
getSpy.calls.reset();
putSpy.calls.reset();
delSpy.calls.reset();
await object.destroy();
expect(getSpy.calls.count()).toBe(2);
expect(putSpy.calls.count()).toBe(1);
expect(delSpy.calls.count()).toBe(1);
const keys = await cacheAdapter.getAllKeys();
expect(keys.length).toBe(0);
});
it('test schema update class', async () => {
const container = new Container();
await container.save();
getSpy.calls.reset();
putSpy.calls.reset();
delSpy.calls.reset();
const config = Config.get('test');
const schema = await config.database.loadSchema();
await schema.reloadData();
const levelPermissions = {
find: { '*': true },
get: { '*': true },
create: { '*': true },
update: { '*': true },
delete: { '*': true },
addField: { '*': true },
protectedFields: { '*': [] },
};
await schema.updateClass(
'Container',
{
fooOne: { type: 'Number' },
fooTwo: { type: 'Array' },
fooThree: { type: 'Date' },
fooFour: { type: 'Object' },
fooFive: { type: 'Relation', targetClass: '_User' },
fooSix: { type: 'String' },
fooSeven: { type: 'Object' },
fooEight: { type: 'String' },
fooNine: { type: 'String' },
fooTeen: { type: 'Number' },
fooEleven: { type: 'String' },
fooTwelve: { type: 'String' },
fooThirteen: { type: 'String' },
fooFourteen: { type: 'String' },
fooFifteen: { type: 'String' },
fooSixteen: { type: 'String' },
fooEighteen: { type: 'String' },
fooNineteen: { type: 'String' },
},
levelPermissions,
{},
config.database
);
expect(getSpy.calls.count()).toBe(3);
expect(putSpy.calls.count()).toBe(3);
expect(delSpy.calls.count()).toBe(0);
const keys = await cacheAdapter.getAllKeys();
expect(keys.length).toBe(1);
});
});

View File

@@ -24,10 +24,6 @@ describe('SchemaController', () => {
config = Config.get('test'); config = Config.get('test');
}); });
afterEach(async () => {
await config.database.schemaCache.clear();
});
it('can validate one object', done => { it('can validate one object', done => {
config.database config.database
.loadSchema() .loadSchema()
@@ -1349,17 +1345,6 @@ describe('SchemaController', () => {
.catch(done.fail); .catch(done.fail);
}); });
it('setAllClasses return classes if cache fails', async () => {
const schema = await config.database.loadSchema();
spyOn(schema._cache, 'setAllClasses').and.callFake(() => Promise.reject('Oops!'));
const errorSpy = spyOn(console, 'error').and.callFake(() => {});
const allSchema = await schema.setAllClasses();
expect(allSchema).toBeDefined();
expect(errorSpy).toHaveBeenCalledWith('Error saving schema to cache:', 'Oops!');
});
it('should not throw on null field types', async () => { it('should not throw on null field types', async () => {
const schema = await config.database.loadSchema(); const schema = await config.database.loadSchema();
const result = await schema.enforceFieldExists('NewClass', 'fieldName', null); const result = await schema.enforceFieldExists('NewClass', 'fieldName', null);

View File

@@ -1,104 +0,0 @@
const CacheController = require('../lib/Controllers/CacheController.js').default;
const InMemoryCacheAdapter = require('../lib/Adapters/Cache/InMemoryCacheAdapter').default;
const SchemaCache = require('../lib/Controllers/SchemaCache').default;
describe('SchemaCache', () => {
let cacheController;
beforeEach(() => {
const cacheAdapter = new InMemoryCacheAdapter({});
cacheController = new CacheController(cacheAdapter, 'appId');
});
it('can retrieve a single schema after all schemas stored', done => {
const schemaCache = new SchemaCache(cacheController);
const allSchemas = [
{
className: 'Class1',
},
{
className: 'Class2',
},
];
schemaCache
.setAllClasses(allSchemas)
.then(() => {
return schemaCache.getOneSchema('Class2');
})
.then(schema => {
expect(schema).not.toBeNull();
done();
});
});
it("doesn't persist cached data by default", done => {
const schemaCache = new SchemaCache(cacheController);
const schema = {
className: 'Class1',
};
schemaCache.setAllClasses([schema]).then(() => {
const anotherSchemaCache = new SchemaCache(cacheController);
return anotherSchemaCache.getOneSchema(schema.className).then(schema => {
expect(schema).toBeNull();
done();
});
});
});
it('can persist cached data', done => {
const schemaCache = new SchemaCache(cacheController, 5000, true);
const schema = {
className: 'Class1',
};
schemaCache.setAllClasses([schema]).then(() => {
const anotherSchemaCache = new SchemaCache(cacheController, 5000, true);
return anotherSchemaCache.getOneSchema(schema.className).then(schema => {
expect(schema).not.toBeNull();
done();
});
});
});
it('should not store if ttl is null', async () => {
const ttl = null;
const schemaCache = new SchemaCache(cacheController, ttl);
expect(await schemaCache.getAllClasses()).toBeNull();
expect(await schemaCache.setAllClasses()).toBeNull();
expect(await schemaCache.getOneSchema()).toBeNull();
});
it('should convert string ttl to number', async () => {
const ttl = '5000';
const schemaCache = new SchemaCache(cacheController, ttl);
expect(schemaCache.ttl).toBe(5000);
});
it('should use the SchemaCache ttl', async () => {
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
const anotherCacheAdapter = new InMemoryCacheAdapter({ ttl: 2000 });
const anotherCacheController = new CacheController(anotherCacheAdapter, 'appId');
const schemaCacheTTL = 5000;
const schemaCache = new SchemaCache(anotherCacheController, schemaCacheTTL, true);
const schema = {
className: 'Class1',
};
await schemaCache.setAllClasses([schema]);
await sleep(4000);
expect(await schemaCache.getOneSchema(schema.className)).not.toBeNull();
});
it('should be expired', async () => {
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
const schemaCacheTTL = 2000;
const schemaCache = new SchemaCache(cacheController, schemaCacheTTL, true);
const schema = {
className: 'Class1',
};
await schemaCache.setAllClasses([schema]);
await sleep(3000);
expect(await schemaCache.getOneSchema(schema.className)).toBeNull();
});
});

View File

@@ -0,0 +1,209 @@
const Config = require('../lib/Config');
const MongoStorageAdapter = require('../lib/Adapters/Storage/Mongo/MongoStorageAdapter').default;
const mongoURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase';
describe_only_db('mongo')('Schema Performance', function () {
let getAllSpy;
let config;
beforeEach(async () => {
config = Config.get('test');
config.schemaCache.clear();
const databaseAdapter = new MongoStorageAdapter({ uri: mongoURI });
await reconfigureServer({ databaseAdapter });
getAllSpy = spyOn(databaseAdapter, 'getAllClasses').and.callThrough();
});
it('test new object', async () => {
const object = new TestObject();
object.set('foo', 'bar');
await object.save();
expect(getAllSpy.calls.count()).toBe(2);
});
it('test new object multiple fields', async () => {
const container = new Container({
dateField: new Date(),
arrayField: [],
numberField: 1,
stringField: 'hello',
booleanField: true,
});
await container.save();
expect(getAllSpy.calls.count()).toBe(2);
});
it('test update existing fields', async () => {
const object = new TestObject();
object.set('foo', 'bar');
await object.save();
getAllSpy.calls.reset();
object.set('foo', 'barz');
await object.save();
expect(getAllSpy.calls.count()).toBe(0);
});
xit('test saveAll / destroyAll', async () => {
// This test can be flaky due to the nature of /batch requests
// Used for performance
const object = new TestObject();
await object.save();
getAllSpy.calls.reset();
const objects = [];
for (let i = 0; i < 10; i++) {
const object = new TestObject();
object.set('number', i);
objects.push(object);
}
await Parse.Object.saveAll(objects);
expect(getAllSpy.calls.count()).toBe(0);
getAllSpy.calls.reset();
const query = new Parse.Query(TestObject);
await query.find();
expect(getAllSpy.calls.count()).toBe(0);
getAllSpy.calls.reset();
await Parse.Object.destroyAll(objects);
expect(getAllSpy.calls.count()).toBe(0);
});
it('test add new field to existing object', async () => {
const object = new TestObject();
object.set('foo', 'bar');
await object.save();
getAllSpy.calls.reset();
object.set('new', 'barz');
await object.save();
expect(getAllSpy.calls.count()).toBe(1);
});
it('test add multiple fields to existing object', async () => {
const object = new TestObject();
object.set('foo', 'bar');
await object.save();
getAllSpy.calls.reset();
object.set({
dateField: new Date(),
arrayField: [],
numberField: 1,
stringField: 'hello',
booleanField: true,
});
await object.save();
expect(getAllSpy.calls.count()).toBe(1);
});
it('test user', async () => {
const user = new Parse.User();
user.setUsername('testing');
user.setPassword('testing');
await user.signUp();
expect(getAllSpy.calls.count()).toBe(1);
});
it('test query include', async () => {
const child = new TestObject();
await child.save();
const object = new TestObject();
object.set('child', child);
await object.save();
getAllSpy.calls.reset();
const query = new Parse.Query(TestObject);
query.include('child');
await query.get(object.id);
expect(getAllSpy.calls.count()).toBe(0);
});
it('query relation without schema', async () => {
const child = new Parse.Object('ChildObject');
await child.save();
const parent = new Parse.Object('ParentObject');
const relation = parent.relation('child');
relation.add(child);
await parent.save();
getAllSpy.calls.reset();
const objects = await relation.query().find();
expect(objects.length).toBe(1);
expect(objects[0].id).toBe(child.id);
expect(getAllSpy.calls.count()).toBe(0);
});
it('test delete object', async () => {
const object = new TestObject();
object.set('foo', 'bar');
await object.save();
getAllSpy.calls.reset();
await object.destroy();
expect(getAllSpy.calls.count()).toBe(0);
});
it('test schema update class', async () => {
const container = new Container();
await container.save();
getAllSpy.calls.reset();
const schema = await config.database.loadSchema();
await schema.reloadData();
const levelPermissions = {
find: { '*': true },
get: { '*': true },
create: { '*': true },
update: { '*': true },
delete: { '*': true },
addField: { '*': true },
protectedFields: { '*': [] },
};
await schema.updateClass(
'Container',
{
fooOne: { type: 'Number' },
fooTwo: { type: 'Array' },
fooThree: { type: 'Date' },
fooFour: { type: 'Object' },
fooFive: { type: 'Relation', targetClass: '_User' },
fooSix: { type: 'String' },
fooSeven: { type: 'Object' },
fooEight: { type: 'String' },
fooNine: { type: 'String' },
fooTeen: { type: 'Number' },
fooEleven: { type: 'String' },
fooTwelve: { type: 'String' },
fooThirteen: { type: 'String' },
fooFourteen: { type: 'String' },
fooFifteen: { type: 'String' },
fooSixteen: { type: 'String' },
fooEighteen: { type: 'String' },
fooNineteen: { type: 'String' },
},
levelPermissions,
{},
config.database
);
expect(getAllSpy.calls.count()).toBe(0);
});
});

View File

@@ -4,7 +4,9 @@ const WinstonLoggerAdapter = require('../lib/Adapters/Logger/WinstonLoggerAdapte
.WinstonLoggerAdapter; .WinstonLoggerAdapter;
const request = require('../lib/request'); const request = require('../lib/request');
describe('info logs', () => { describe_only(() => {
return process.env.PARSE_SERVER_LOG_LEVEL !== 'debug';
})('info logs', () => {
it('Verify INFO logs', done => { it('Verify INFO logs', done => {
const winstonLoggerAdapter = new WinstonLoggerAdapter(); const winstonLoggerAdapter = new WinstonLoggerAdapter();
winstonLoggerAdapter.log('info', 'testing info logs with 1234'); winstonLoggerAdapter.log('info', 'testing info logs with 1234');
@@ -85,7 +87,9 @@ describe('info logs', () => {
}); });
}); });
describe('error logs', () => { describe_only(() => {
return process.env.PARSE_SERVER_LOG_LEVEL !== 'debug';
})('error logs', () => {
it('Verify ERROR logs', done => { it('Verify ERROR logs', done => {
const winstonLoggerAdapter = new WinstonLoggerAdapter(); const winstonLoggerAdapter = new WinstonLoggerAdapter();
winstonLoggerAdapter.log('error', 'testing error logs'); winstonLoggerAdapter.log('error', 'testing error logs');
@@ -167,7 +171,9 @@ describe('error logs', () => {
}); });
}); });
describe('verbose logs', () => { describe_only(() => {
return process.env.PARSE_SERVER_LOG_LEVEL !== 'debug';
})('verbose logs', () => {
it('mask sensitive information in _User class', done => { it('mask sensitive information in _User class', done => {
reconfigureServer({ verbose: true }) reconfigureServer({ verbose: true })
.then(() => createTestUser()) .then(() => createTestUser())

View File

@@ -2,6 +2,7 @@
const semver = require('semver'); const semver = require('semver');
const CurrentSpecReporter = require('./support/CurrentSpecReporter.js'); const CurrentSpecReporter = require('./support/CurrentSpecReporter.js');
const { SpecReporter } = require('jasmine-spec-reporter'); const { SpecReporter } = require('jasmine-spec-reporter');
const SchemaCache = require('../lib/Adapters/Cache/SchemaCache').default;
// Sets up a Parse API server for testing. // Sets up a Parse API server for testing.
jasmine.DEFAULT_TIMEOUT_INTERVAL = process.env.PARSE_SERVER_TEST_TIMEOUT || 10000; jasmine.DEFAULT_TIMEOUT_INTERVAL = process.env.PARSE_SERVER_TEST_TIMEOUT || 10000;
@@ -206,6 +207,7 @@ afterEach(function (done) {
} }
destroyAliveConnections(); destroyAliveConnections();
await TestUtils.destroyAllDataPermanently(true); await TestUtils.destroyAllDataPermanently(true);
SchemaCache.clear();
if (didChangeConfiguration) { if (didChangeConfiguration) {
await reconfigureServer(); await reconfigureServer();
} else { } else {

View File

@@ -70,6 +70,8 @@ describe('server', () => {
}, },
}), }),
}).catch(() => { }).catch(() => {
const config = Config.get('test');
config.schemaCache.clear();
//Need to use rest api because saving via JS SDK results in fail() not getting called //Need to use rest api because saving via JS SDK results in fail() not getting called
request({ request({
method: 'POST', method: 'POST',

View File

@@ -145,10 +145,6 @@ describe('schemas', () => {
config = Config.get('test'); config = Config.get('test');
}); });
afterEach(async () => {
await config.database.schemaCache.clear();
});
it('requires the master key to get all schemas', done => { it('requires the master key to get all schemas', done => {
request({ request({
url: 'http://localhost:8378/1/schemas', url: 'http://localhost:8378/1/schemas',

View File

@@ -4,12 +4,9 @@ const Parse = require('parse/node');
const className = 'AnObject'; const className = 'AnObject';
const defaultRoleName = 'tester'; const defaultRoleName = 'tester';
let schemaCache;
module.exports = { module.exports = {
/* AnObject */ /* AnObject */
className, className,
schemaCache,
/** /**
* Creates and returns new user. * Creates and returns new user.

View File

@@ -8,7 +8,7 @@ function validateAuthData(authData) {
const apiURL = authData.apiURL || defaultURL; const apiURL = authData.apiURL || defaultURL;
const path = `${apiURL}me?fields=id&access_token=${authData.access_token}`; const path = `${apiURL}me?fields=id&access_token=${authData.access_token}`;
return httpsRequest.get(path).then(response => { return httpsRequest.get(path).then(response => {
const user = response.data ? response.data : response const user = response.data ? response.data : response;
if (user && user.id == authData.id) { if (user && user.id == authData.id) {
return; return;
} }

View File

@@ -0,0 +1,23 @@
const SchemaCache = {};
export default {
all() {
return [...(SchemaCache.allClasses || [])];
},
get(className) {
return this.all().find(cached => cached.className === className);
},
put(allSchema) {
SchemaCache.allClasses = allSchema;
},
del(className) {
this.put(this.all().filter(cached => cached.className !== className));
},
clear() {
delete SchemaCache.allClasses;
},
};

View File

@@ -113,12 +113,15 @@ export class MongoStorageAdapter implements StorageAdapter {
_uri: string; _uri: string;
_collectionPrefix: string; _collectionPrefix: string;
_mongoOptions: Object; _mongoOptions: Object;
_onchange: any;
_stream: any;
// Public // Public
connectionPromise: ?Promise<any>; connectionPromise: ?Promise<any>;
database: any; database: any;
client: MongoClient; client: MongoClient;
_maxTimeMS: ?number; _maxTimeMS: ?number;
canSortOnJoinTables: boolean; canSortOnJoinTables: boolean;
enableSchemaHooks: boolean;
constructor({ uri = defaults.DefaultMongoURI, collectionPrefix = '', mongoOptions = {} }: any) { constructor({ uri = defaults.DefaultMongoURI, collectionPrefix = '', mongoOptions = {} }: any) {
this._uri = uri; this._uri = uri;
@@ -126,13 +129,20 @@ export class MongoStorageAdapter implements StorageAdapter {
this._mongoOptions = mongoOptions; this._mongoOptions = mongoOptions;
this._mongoOptions.useNewUrlParser = true; this._mongoOptions.useNewUrlParser = true;
this._mongoOptions.useUnifiedTopology = true; this._mongoOptions.useUnifiedTopology = true;
this._onchange = () => {};
// MaxTimeMS is not a global MongoDB client option, it is applied per operation. // MaxTimeMS is not a global MongoDB client option, it is applied per operation.
this._maxTimeMS = mongoOptions.maxTimeMS; this._maxTimeMS = mongoOptions.maxTimeMS;
this.canSortOnJoinTables = true; this.canSortOnJoinTables = true;
this.enableSchemaHooks = !!mongoOptions.enableSchemaHooks;
delete mongoOptions.enableSchemaHooks;
delete mongoOptions.maxTimeMS; delete mongoOptions.maxTimeMS;
} }
watch(callback: () => void): void {
this._onchange = callback;
}
connect() { connect() {
if (this.connectionPromise) { if (this.connectionPromise) {
return this.connectionPromise; return this.connectionPromise;
@@ -198,7 +208,13 @@ export class MongoStorageAdapter implements StorageAdapter {
_schemaCollection(): Promise<MongoSchemaCollection> { _schemaCollection(): Promise<MongoSchemaCollection> {
return this.connect() return this.connect()
.then(() => this._adaptiveCollection(MongoSchemaCollectionName)) .then(() => this._adaptiveCollection(MongoSchemaCollectionName))
.then(collection => new MongoSchemaCollection(collection)); .then(collection => {
if (!this._stream && this.enableSchemaHooks) {
this._stream = collection._mongoCollection.watch();
this._stream.on('change', () => this._onchange());
}
return new MongoSchemaCollection(collection);
});
} }
classExists(name: string) { classExists(name: string) {

View File

@@ -4,6 +4,8 @@ import { createClient } from './PostgresClient';
import Parse from 'parse/node'; import Parse from 'parse/node';
// @flow-disable-next // @flow-disable-next
import _ from 'lodash'; import _ from 'lodash';
// @flow-disable-next
import { v4 as uuidv4 } from 'uuid';
import sql from './sql'; import sql from './sql';
const PostgresRelationDoesNotExistError = '42P01'; const PostgresRelationDoesNotExistError = '42P01';
@@ -794,20 +796,33 @@ const buildWhereClause = ({ schema, query, index, caseInsensitive }): WhereClaus
export class PostgresStorageAdapter implements StorageAdapter { export class PostgresStorageAdapter implements StorageAdapter {
canSortOnJoinTables: boolean; canSortOnJoinTables: boolean;
enableSchemaHooks: boolean;
// Private // Private
_collectionPrefix: string; _collectionPrefix: string;
_client: any; _client: any;
_onchange: any;
_pgp: any; _pgp: any;
_stream: any;
_uuid: any;
constructor({ uri, collectionPrefix = '', databaseOptions }: any) { constructor({ uri, collectionPrefix = '', databaseOptions = {} }: any) {
this._collectionPrefix = collectionPrefix; this._collectionPrefix = collectionPrefix;
this.enableSchemaHooks = !!databaseOptions.enableSchemaHooks;
delete databaseOptions.enableSchemaHooks;
const { client, pgp } = createClient(uri, databaseOptions); const { client, pgp } = createClient(uri, databaseOptions);
this._client = client; this._client = client;
this._onchange = () => {};
this._pgp = pgp; this._pgp = pgp;
this._uuid = uuidv4();
this.canSortOnJoinTables = false; this.canSortOnJoinTables = false;
} }
watch(callback: () => void): void {
this._onchange = callback;
}
//Note that analyze=true will run the query, executing INSERTS, DELETES, etc. //Note that analyze=true will run the query, executing INSERTS, DELETES, etc.
createExplainableQuery(query: string, analyze: boolean = false) { createExplainableQuery(query: string, analyze: boolean = false) {
if (analyze) { if (analyze) {
@@ -818,12 +833,39 @@ export class PostgresStorageAdapter implements StorageAdapter {
} }
handleShutdown() { handleShutdown() {
if (this._stream) {
this._stream.done();
delete this._stream;
}
if (!this._client) { if (!this._client) {
return; return;
} }
this._client.$pool.end(); this._client.$pool.end();
} }
async _listenToSchema() {
if (!this._stream && this.enableSchemaHooks) {
this._stream = await this._client.connect({ direct: true });
this._stream.client.on('notification', data => {
const payload = JSON.parse(data.payload);
if (payload.senderId !== this._uuid) {
this._onchange();
}
});
await this._stream.none('LISTEN $1~', 'schema.change');
}
}
_notifySchemaChange() {
if (this._stream) {
this._stream
.none('NOTIFY $1~, $2', ['schema.change', { senderId: this._uuid }])
.catch(error => {
console.log('Failed to Notify:', error); // unlikely to ever happen
});
}
}
async _ensureSchemaCollectionExists(conn: any) { async _ensureSchemaCollectionExists(conn: any) {
conn = conn || this._client; conn = conn || this._client;
await conn await conn
@@ -859,6 +901,7 @@ export class PostgresStorageAdapter implements StorageAdapter {
values values
); );
}); });
this._notifySchemaChange();
} }
async setIndexesWithSchemaFormat( async setIndexesWithSchemaFormat(
@@ -920,11 +963,12 @@ export class PostgresStorageAdapter implements StorageAdapter {
[className, 'schema', 'indexes', JSON.stringify(existingIndexes)] [className, 'schema', 'indexes', JSON.stringify(existingIndexes)]
); );
}); });
this._notifySchemaChange();
} }
async createClass(className: string, schema: SchemaType, conn: ?any) { async createClass(className: string, schema: SchemaType, conn: ?any) {
conn = conn || this._client; conn = conn || this._client;
return conn const parseSchema = await conn
.tx('create-class', async t => { .tx('create-class', async t => {
await this.createTable(className, schema, t); await this.createTable(className, schema, t);
await t.none( await t.none(
@@ -940,6 +984,8 @@ export class PostgresStorageAdapter implements StorageAdapter {
} }
throw err; throw err;
}); });
this._notifySchemaChange();
return parseSchema;
} }
// Just create a table, do not insert in schema // Just create a table, do not insert in schema
@@ -1073,6 +1119,7 @@ export class PostgresStorageAdapter implements StorageAdapter {
); );
} }
}); });
this._notifySchemaChange();
} }
// Drops a collection. Resolves with true if it was a Parse Schema (eg. _User, Custom, etc.) // Drops a collection. Resolves with true if it was a Parse Schema (eg. _User, Custom, etc.)
@@ -1085,9 +1132,12 @@ export class PostgresStorageAdapter implements StorageAdapter {
values: [className], values: [className],
}, },
]; ];
return this._client const response = await this._client
.tx(t => t.none(this._pgp.helpers.concat(operations))) .tx(t => t.none(this._pgp.helpers.concat(operations)))
.then(() => className.indexOf('_Join:') != 0); // resolves with false when _Join table .then(() => className.indexOf('_Join:') != 0); // resolves with false when _Join table
this._notifySchemaChange();
return response;
} }
// Delete all data known to this adapter. Used for testing. // Delete all data known to this adapter. Used for testing.
@@ -1173,6 +1223,7 @@ export class PostgresStorageAdapter implements StorageAdapter {
await t.none(`ALTER TABLE $1:name DROP COLUMN IF EXISTS ${columns}`, values); await t.none(`ALTER TABLE $1:name DROP COLUMN IF EXISTS ${columns}`, values);
} }
}); });
this._notifySchemaChange();
} }
// Return a promise for all schemas known to this adapter, in Parse format. In case the // Return a promise for all schemas known to this adapter, in Parse format. In case the
@@ -2237,6 +2288,7 @@ export class PostgresStorageAdapter implements StorageAdapter {
}) })
.then(() => this.schemaUpgrade(schema.className, schema)); .then(() => this.schemaUpgrade(schema.className, schema));
}); });
promises.push(this._listenToSchema());
return Promise.all(promises) return Promise.all(promises)
.then(() => { .then(() => {
return this._client.tx('perform-initialization', async t => { return this._client.tx('perform-initialization', async t => {

View File

@@ -111,6 +111,7 @@ export interface StorageAdapter {
explain?: boolean explain?: boolean
): Promise<any>; ): Promise<any>;
performInitialization(options: ?any): Promise<void>; performInitialization(options: ?any): Promise<void>;
watch(callback: () => void): void;
// Indexing // Indexing
createIndexes(className: string, indexes: any, conn: ?any): Promise<void>; createIndexes(className: string, indexes: any, conn: ?any): Promise<void>;

View File

@@ -3,7 +3,6 @@
// 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 AppCache from './cache'; import AppCache from './cache';
import SchemaCache from './Controllers/SchemaCache';
import DatabaseController from './Controllers/DatabaseController'; import DatabaseController from './Controllers/DatabaseController';
import net from 'net'; import net from 'net';
import { import {
@@ -35,12 +34,7 @@ export class Config {
config.applicationId = applicationId; config.applicationId = applicationId;
Object.keys(cacheInfo).forEach(key => { Object.keys(cacheInfo).forEach(key => {
if (key == 'databaseController') { if (key == 'databaseController') {
const schemaCache = new SchemaCache( config.database = new DatabaseController(cacheInfo.databaseController.adapter);
cacheInfo.cacheController,
cacheInfo.schemaCacheTTL,
cacheInfo.enableSingleSchemaCache
);
config.database = new DatabaseController(cacheInfo.databaseController.adapter, schemaCache);
} else { } else {
config[key] = cacheInfo[key]; config[key] = cacheInfo[key];
} }

View File

@@ -13,6 +13,9 @@ import deepcopy from 'deepcopy';
import logger from '../logger'; import logger from '../logger';
import * as SchemaController from './SchemaController'; import * as SchemaController from './SchemaController';
import { StorageAdapter } from '../Adapters/Storage/StorageAdapter'; import { StorageAdapter } from '../Adapters/Storage/StorageAdapter';
import MongoStorageAdapter from '../Adapters/Storage/Mongo/MongoStorageAdapter';
import SchemaCache from '../Adapters/Cache/SchemaCache';
import type { LoadSchemaOptions } from './types';
import type { QueryOptions, FullQueryOptions } from '../Adapters/Storage/StorageAdapter'; import type { QueryOptions, FullQueryOptions } from '../Adapters/Storage/StorageAdapter';
function addWriteACL(query, acl) { function addWriteACL(query, acl) {
@@ -230,9 +233,6 @@ const filterSensitiveData = (
return object; return object;
}; };
import type { LoadSchemaOptions } from './types';
import MongoStorageAdapter from '../Adapters/Storage/Mongo/MongoStorageAdapter';
// Runs an update on the database. // Runs an update on the database.
// Returns a promise for an object with the new values for field // Returns a promise for an object with the new values for field
// modifications that don't know their results ahead of time, like // modifications that don't know their results ahead of time, like
@@ -398,9 +398,8 @@ class DatabaseController {
schemaPromise: ?Promise<SchemaController.SchemaController>; schemaPromise: ?Promise<SchemaController.SchemaController>;
_transactionalSession: ?any; _transactionalSession: ?any;
constructor(adapter: StorageAdapter, schemaCache: any) { constructor(adapter: StorageAdapter) {
this.adapter = adapter; this.adapter = adapter;
this.schemaCache = schemaCache;
// We don't want a mutable this.schema, because then you could have // We don't want a mutable this.schema, because then you could have
// one request that uses different schemas for different parts of // one request that uses different schemas for different parts of
// it. Instead, use loadSchema to get a schema. // it. Instead, use loadSchema to get a schema.
@@ -434,7 +433,7 @@ class DatabaseController {
if (this.schemaPromise != null) { if (this.schemaPromise != null) {
return this.schemaPromise; return this.schemaPromise;
} }
this.schemaPromise = SchemaController.load(this.adapter, this.schemaCache, options); this.schemaPromise = SchemaController.load(this.adapter, options);
this.schemaPromise.then( this.schemaPromise.then(
() => delete this.schemaPromise, () => delete this.schemaPromise,
() => delete this.schemaPromise () => delete this.schemaPromise
@@ -916,7 +915,8 @@ class DatabaseController {
*/ */
deleteEverything(fast: boolean = false): Promise<any> { deleteEverything(fast: boolean = false): Promise<any> {
this.schemaPromise = null; this.schemaPromise = null;
return Promise.all([this.adapter.deleteAllClasses(fast), this.schemaCache.clear()]); SchemaCache.clear();
return this.adapter.deleteAllClasses(fast);
} }
// Returns a promise for a list of related ids given an owning id. // Returns a promise for a list of related ids given an owning id.
@@ -1325,8 +1325,12 @@ class DatabaseController {
} }
deleteSchema(className: string): Promise<void> { deleteSchema(className: string): Promise<void> {
let schemaController;
return this.loadSchema({ clearCache: true }) return this.loadSchema({ clearCache: true })
.then(schemaController => schemaController.getOneSchema(className, true)) .then(s => {
schemaController = s;
return schemaController.getOneSchema(className, true);
})
.catch(error => { .catch(error => {
if (error === undefined) { if (error === undefined) {
return { fields: {} }; return { fields: {} };
@@ -1356,7 +1360,8 @@ class DatabaseController {
this.adapter.deleteClass(joinTableName(className, name)) this.adapter.deleteClass(joinTableName(className, name))
) )
).then(() => { ).then(() => {
return; SchemaCache.del(className);
return schemaController.reloadData();
}); });
} else { } else {
return Promise.resolve(); return Promise.resolve();
@@ -1688,108 +1693,64 @@ class DatabaseController {
...SchemaController.defaultColumns._Idempotency, ...SchemaController.defaultColumns._Idempotency,
}, },
}; };
await this.loadSchema().then(schema => schema.enforceClassExists('_User'));
await this.loadSchema().then(schema => schema.enforceClassExists('_Role'));
if (this.adapter instanceof MongoStorageAdapter) {
await this.loadSchema().then(schema => schema.enforceClassExists('_Idempotency'));
}
const userClassPromise = this.loadSchema().then(schema => schema.enforceClassExists('_User')); await this.adapter.ensureUniqueness('_User', requiredUserFields, ['username']).catch(error => {
const roleClassPromise = this.loadSchema().then(schema => schema.enforceClassExists('_Role')); logger.warn('Unable to ensure uniqueness for usernames: ', error);
const idempotencyClassPromise = throw error;
this.adapter instanceof MongoStorageAdapter });
? this.loadSchema().then(schema => schema.enforceClassExists('_Idempotency'))
: Promise.resolve();
const usernameUniqueness = userClassPromise await this.adapter
.then(() => this.adapter.ensureUniqueness('_User', requiredUserFields, ['username'])) .ensureIndex('_User', requiredUserFields, ['username'], 'case_insensitive_username', true)
.catch(error => { .catch(error => {
logger.warn('Unable to ensure uniqueness for usernames: ', error); logger.warn('Unable to create case insensitive username index: ', error);
throw error; throw error;
}); });
await this.adapter
const usernameCaseInsensitiveIndex = userClassPromise .ensureIndex('_User', requiredUserFields, ['username'], 'case_insensitive_username', true)
.then(() =>
this.adapter.ensureIndex(
'_User',
requiredUserFields,
['username'],
'case_insensitive_username',
true
)
)
.catch(error => { .catch(error => {
logger.warn('Unable to create case insensitive username index: ', error); logger.warn('Unable to create case insensitive username index: ', error);
throw error; throw error;
}); });
const emailUniqueness = userClassPromise await this.adapter.ensureUniqueness('_User', requiredUserFields, ['email']).catch(error => {
.then(() => this.adapter.ensureUniqueness('_User', requiredUserFields, ['email'])) logger.warn('Unable to ensure uniqueness for user email addresses: ', error);
.catch(error => { throw error;
logger.warn('Unable to ensure uniqueness for user email addresses: ', error); });
throw error;
});
const emailCaseInsensitiveIndex = userClassPromise await this.adapter
.then(() => .ensureIndex('_User', requiredUserFields, ['email'], 'case_insensitive_email', true)
this.adapter.ensureIndex(
'_User',
requiredUserFields,
['email'],
'case_insensitive_email',
true
)
)
.catch(error => { .catch(error => {
logger.warn('Unable to create case insensitive email index: ', error); logger.warn('Unable to create case insensitive email index: ', error);
throw error; throw error;
}); });
const roleUniqueness = roleClassPromise await this.adapter.ensureUniqueness('_Role', requiredRoleFields, ['name']).catch(error => {
.then(() => this.adapter.ensureUniqueness('_Role', requiredRoleFields, ['name'])) logger.warn('Unable to ensure uniqueness for role name: ', error);
.catch(error => { throw error;
logger.warn('Unable to ensure uniqueness for role name: ', error); });
throw error; if (this.adapter instanceof MongoStorageAdapter) {
}); await this.adapter
.ensureUniqueness('_Idempotency', requiredIdempotencyFields, ['reqId'])
.catch(error => {
logger.warn('Unable to ensure uniqueness for idempotency request ID: ', error);
throw error;
});
const idempotencyRequestIdIndex = await this.adapter
this.adapter instanceof MongoStorageAdapter .ensureIndex('_Idempotency', requiredIdempotencyFields, ['expire'], 'ttl', false, {
? idempotencyClassPromise ttl: 0,
.then(() => })
this.adapter.ensureUniqueness('_Idempotency', requiredIdempotencyFields, ['reqId']) .catch(error => {
) logger.warn('Unable to create TTL index for idempotency expire date: ', error);
.catch(error => { throw error;
logger.warn('Unable to ensure uniqueness for idempotency request ID: ', error); });
throw error; }
}) await this.adapter.updateSchemaWithIndexes();
: Promise.resolve();
const idempotencyExpireIndex =
this.adapter instanceof MongoStorageAdapter
? idempotencyClassPromise
.then(() =>
this.adapter.ensureIndex(
'_Idempotency',
requiredIdempotencyFields,
['expire'],
'ttl',
false,
{ ttl: 0 }
)
)
.catch(error => {
logger.warn('Unable to create TTL index for idempotency expire date: ', error);
throw error;
})
: Promise.resolve();
const indexPromise = this.adapter.updateSchemaWithIndexes();
return Promise.all([
usernameUniqueness,
usernameCaseInsensitiveIndex,
emailUniqueness,
emailCaseInsensitiveIndex,
roleUniqueness,
idempotencyRequestIdIndex,
idempotencyExpireIndex,
indexPromise,
]);
} }
static _validateQuery: any => void; static _validateQuery: any => void;

View File

@@ -1,55 +0,0 @@
const MAIN_SCHEMA = '__MAIN_SCHEMA';
const SCHEMA_CACHE_PREFIX = '__SCHEMA';
import { randomString } from '../cryptoUtils';
import defaults from '../defaults';
export default class SchemaCache {
cache: Object;
constructor(cacheController, ttl = defaults.schemaCacheTTL, singleCache = false) {
this.ttl = ttl;
if (typeof ttl == 'string') {
this.ttl = parseInt(ttl);
}
this.cache = cacheController;
this.prefix = SCHEMA_CACHE_PREFIX;
if (!singleCache) {
this.prefix += randomString(20);
}
}
getAllClasses() {
if (!this.ttl) {
return Promise.resolve(null);
}
return this.cache.get(this.prefix + MAIN_SCHEMA);
}
setAllClasses(schema) {
if (!this.ttl) {
return Promise.resolve(null);
}
return this.cache.put(this.prefix + MAIN_SCHEMA, schema, this.ttl);
}
getOneSchema(className) {
if (!this.ttl) {
return Promise.resolve(null);
}
return this.cache.get(this.prefix + MAIN_SCHEMA).then(cachedSchemas => {
cachedSchemas = cachedSchemas || [];
const schema = cachedSchemas.find(cachedSchema => {
return cachedSchema.className === className;
});
if (schema) {
return Promise.resolve(schema);
}
return Promise.resolve(null);
});
}
clear() {
return this.cache.del(this.prefix + MAIN_SCHEMA);
}
}

View File

@@ -17,6 +17,7 @@
// @flow-disable-next // @flow-disable-next
const Parse = require('parse/node').Parse; const Parse = require('parse/node').Parse;
import { StorageAdapter } from '../Adapters/Storage/StorageAdapter'; import { StorageAdapter } from '../Adapters/Storage/StorageAdapter';
import SchemaCache from '../Adapters/Cache/SchemaCache';
import DatabaseController from './DatabaseController'; import DatabaseController from './DatabaseController';
import Config from '../Config'; import Config from '../Config';
// @flow-disable-next // @flow-disable-next
@@ -682,15 +683,13 @@ const typeToString = (type: SchemaField | string): string => {
export default class SchemaController { export default class SchemaController {
_dbAdapter: StorageAdapter; _dbAdapter: StorageAdapter;
schemaData: { [string]: Schema }; schemaData: { [string]: Schema };
_cache: any;
reloadDataPromise: ?Promise<any>; reloadDataPromise: ?Promise<any>;
protectedFields: any; protectedFields: any;
userIdRegEx: RegExp; userIdRegEx: RegExp;
constructor(databaseAdapter: StorageAdapter, schemaCache: any) { constructor(databaseAdapter: StorageAdapter) {
this._dbAdapter = databaseAdapter; this._dbAdapter = databaseAdapter;
this._cache = schemaCache; this.schemaData = new SchemaData(SchemaCache.all(), this.protectedFields);
this.schemaData = new SchemaData();
this.protectedFields = Config.get(Parse.applicationId).protectedFields; this.protectedFields = Config.get(Parse.applicationId).protectedFields;
const customIds = Config.get(Parse.applicationId).allowCustomObjectId; const customIds = Config.get(Parse.applicationId).allowCustomObjectId;
@@ -699,6 +698,10 @@ export default class SchemaController {
const autoIdRegEx = /^[a-zA-Z0-9]{1,}$/; const autoIdRegEx = /^[a-zA-Z0-9]{1,}$/;
this.userIdRegEx = customIds ? customIdRegEx : autoIdRegEx; this.userIdRegEx = customIds ? customIdRegEx : autoIdRegEx;
this._dbAdapter.watch(() => {
this.reloadData({ clearCache: true });
});
} }
reloadData(options: LoadSchemaOptions = { clearCache: false }): Promise<any> { reloadData(options: LoadSchemaOptions = { clearCache: false }): Promise<any> {
@@ -725,12 +728,11 @@ export default class SchemaController {
if (options.clearCache) { if (options.clearCache) {
return this.setAllClasses(); return this.setAllClasses();
} }
return this._cache.getAllClasses().then(allClasses => { const cached = SchemaCache.all();
if (allClasses && allClasses.length) { if (cached && cached.length) {
return Promise.resolve(allClasses); return Promise.resolve(cached);
} }
return this.setAllClasses(); return this.setAllClasses();
});
} }
setAllClasses(): Promise<Array<Schema>> { setAllClasses(): Promise<Array<Schema>> {
@@ -738,11 +740,7 @@ export default class SchemaController {
.getAllClasses() .getAllClasses()
.then(allSchemas => allSchemas.map(injectDefaultSchema)) .then(allSchemas => allSchemas.map(injectDefaultSchema))
.then(allSchemas => { .then(allSchemas => {
/* eslint-disable no-console */ SchemaCache.put(allSchemas);
this._cache
.setAllClasses(allSchemas)
.catch(error => console.error('Error saving schema to cache:', error));
/* eslint-enable no-console */
return allSchemas; return allSchemas;
}); });
} }
@@ -752,32 +750,28 @@ export default class SchemaController {
allowVolatileClasses: boolean = false, allowVolatileClasses: boolean = false,
options: LoadSchemaOptions = { clearCache: false } options: LoadSchemaOptions = { clearCache: false }
): Promise<Schema> { ): Promise<Schema> {
let promise = Promise.resolve();
if (options.clearCache) { if (options.clearCache) {
promise = this._cache.clear(); SchemaCache.clear();
} }
return promise.then(() => { if (allowVolatileClasses && volatileClasses.indexOf(className) > -1) {
if (allowVolatileClasses && volatileClasses.indexOf(className) > -1) { const data = this.schemaData[className];
const data = this.schemaData[className]; return Promise.resolve({
return Promise.resolve({ className,
className, fields: data.fields,
fields: data.fields, classLevelPermissions: data.classLevelPermissions,
classLevelPermissions: data.classLevelPermissions, indexes: data.indexes,
indexes: data.indexes,
});
}
return this._cache.getOneSchema(className).then(cached => {
if (cached && !options.clearCache) {
return Promise.resolve(cached);
}
return this.setAllClasses().then(allSchemas => {
const oneSchema = allSchemas.find(schema => schema.className === className);
if (!oneSchema) {
return Promise.reject(undefined);
}
return oneSchema;
});
}); });
}
const cached = SchemaCache.get(className);
if (cached && !options.clearCache) {
return Promise.resolve(cached);
}
return this.setAllClasses().then(allSchemas => {
const oneSchema = allSchemas.find(schema => schema.className === className);
if (!oneSchema) {
return Promise.reject(undefined);
}
return oneSchema;
}); });
} }
@@ -788,7 +782,7 @@ export default class SchemaController {
// on success, and rejects with an error on fail. Ensure you // on success, and rejects with an error on fail. Ensure you
// have authorization (master key, or client class creation // have authorization (master key, or client class creation
// enabled) before calling this function. // enabled) before calling this function.
addClassIfNotExists( async addClassIfNotExists(
className: string, className: string,
fields: SchemaFields = {}, fields: SchemaFields = {},
classLevelPermissions: any, classLevelPermissions: any,
@@ -803,9 +797,8 @@ export default class SchemaController {
} }
return Promise.reject(validationError); return Promise.reject(validationError);
} }
try {
return this._dbAdapter const adapterSchema = await this._dbAdapter.createClass(
.createClass(
className, className,
convertSchemaToAdapterSchema({ convertSchemaToAdapterSchema({
fields, fields,
@@ -813,18 +806,18 @@ export default class SchemaController {
indexes, indexes,
className, className,
}) })
) );
.then(convertAdapterSchemaToParseSchema) // TODO: Remove by updating schema cache directly
.catch(error => { await this.reloadData({ clearCache: true });
if (error && error.code === Parse.Error.DUPLICATE_VALUE) { const parseSchema = convertAdapterSchemaToParseSchema(adapterSchema);
throw new Parse.Error( return parseSchema;
Parse.Error.INVALID_CLASS_NAME, } catch (error) {
`Class ${className} already exists.` if (error && error.code === Parse.Error.DUPLICATE_VALUE) {
); throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`);
} else { } else {
throw error; throw error;
} }
}); }
} }
updateClass( updateClass(
@@ -938,9 +931,8 @@ export default class SchemaController {
} }
// We don't have this class. Update the schema // We don't have this class. Update the schema
return ( return (
// The schema update succeeded. Reload the schema
this.addClassIfNotExists(className) this.addClassIfNotExists(className)
// The schema update succeeded. Reload the schema
.then(() => this.reloadData({ clearCache: true }))
.catch(() => { .catch(() => {
// The schema update failed. This can be okay - it might // The schema update failed. This can be okay - it might
// have failed because there's a race condition and a different // have failed because there's a race condition and a different
@@ -1050,12 +1042,16 @@ export default class SchemaController {
} }
// Sets the Class-level permissions for a given className, which must exist. // Sets the Class-level permissions for a given className, which must exist.
setPermissions(className: string, perms: any, newSchema: SchemaFields) { async setPermissions(className: string, perms: any, newSchema: SchemaFields) {
if (typeof perms === 'undefined') { if (typeof perms === 'undefined') {
return Promise.resolve(); return Promise.resolve();
} }
validateCLP(perms, newSchema, this.userIdRegEx); validateCLP(perms, newSchema, this.userIdRegEx);
return this._dbAdapter.setClassLevelPermissions(className, perms); await this._dbAdapter.setClassLevelPermissions(className, perms);
const cached = SchemaCache.get(className);
if (cached) {
cached.classLevelPermissions = perms;
}
} }
// Returns a promise that resolves successfully to the new schema // Returns a promise that resolves successfully to the new schema
@@ -1203,7 +1199,9 @@ export default class SchemaController {
); );
}); });
}) })
.then(() => this._cache.clear()); .then(() => {
SchemaCache.clear();
});
} }
// Validates an object provided in REST format. // Validates an object provided in REST format.
@@ -1245,6 +1243,7 @@ export default class SchemaController {
const enforceFields = results.filter(result => !!result); const enforceFields = results.filter(result => !!result);
if (enforceFields.length !== 0) { if (enforceFields.length !== 0) {
// TODO: Remove by updating schema cache directly
await this.reloadData({ clearCache: true }); await this.reloadData({ clearCache: true });
} }
this.ensureFields(enforceFields); this.ensureFields(enforceFields);
@@ -1413,12 +1412,8 @@ export default class SchemaController {
} }
// Returns a promise for a new Schema. // Returns a promise for a new Schema.
const load = ( const load = (dbAdapter: StorageAdapter, options: any): Promise<SchemaController> => {
dbAdapter: StorageAdapter, const schema = new SchemaController(dbAdapter);
schemaCache: any,
options: any
): Promise<SchemaController> => {
const schema = new SchemaController(dbAdapter, schemaCache);
return schema.reloadData(options).then(() => schema); return schema.reloadData(options).then(() => schema);
}; };

View File

@@ -15,7 +15,6 @@ import { PushController } from './PushController';
import { PushQueue } from '../Push/PushQueue'; import { PushQueue } from '../Push/PushQueue';
import { PushWorker } from '../Push/PushWorker'; import { PushWorker } from '../Push/PushWorker';
import DatabaseController from './DatabaseController'; import DatabaseController from './DatabaseController';
import SchemaCache from './SchemaCache';
// Adapters // Adapters
import { GridFSBucketAdapter } from '../Adapters/Files/GridFSBucketAdapter'; import { GridFSBucketAdapter } from '../Adapters/Files/GridFSBucketAdapter';
@@ -26,6 +25,7 @@ import MongoStorageAdapter from '../Adapters/Storage/Mongo/MongoStorageAdapter';
import PostgresStorageAdapter from '../Adapters/Storage/Postgres/PostgresStorageAdapter'; import PostgresStorageAdapter from '../Adapters/Storage/Postgres/PostgresStorageAdapter';
import ParsePushAdapter from '@parse/push-adapter'; import ParsePushAdapter from '@parse/push-adapter';
import ParseGraphQLController from './ParseGraphQLController'; import ParseGraphQLController from './ParseGraphQLController';
import SchemaCache from '../Adapters/Cache/SchemaCache';
export function getControllers(options: ParseServerOptions) { export function getControllers(options: ParseServerOptions) {
const loggerController = getLoggerController(options); const loggerController = getLoggerController(options);
@@ -41,7 +41,7 @@ export function getControllers(options: ParseServerOptions) {
const cacheController = getCacheController(options); const cacheController = getCacheController(options);
const analyticsController = getAnalyticsController(options); const analyticsController = getAnalyticsController(options);
const liveQueryController = getLiveQueryController(options); const liveQueryController = getLiveQueryController(options);
const databaseController = getDatabaseController(options, cacheController); const databaseController = getDatabaseController(options);
const hooksController = getHooksController(options, databaseController); const hooksController = getHooksController(options, databaseController);
const authDataManager = getAuthDataManager(options); const authDataManager = getAuthDataManager(options);
const parseGraphQLController = getParseGraphQLController(options, { const parseGraphQLController = getParseGraphQLController(options, {
@@ -64,6 +64,7 @@ export function getControllers(options: ParseServerOptions) {
databaseController, databaseController,
hooksController, hooksController,
authDataManager, authDataManager,
schemaCache: SchemaCache,
}; };
} }
@@ -141,17 +142,8 @@ export function getLiveQueryController(options: ParseServerOptions): LiveQueryCo
return new LiveQueryController(options.liveQuery); return new LiveQueryController(options.liveQuery);
} }
export function getDatabaseController( export function getDatabaseController(options: ParseServerOptions): DatabaseController {
options: ParseServerOptions, const { databaseURI, collectionPrefix, databaseOptions } = options;
cacheController: CacheController
): DatabaseController {
const {
databaseURI,
databaseOptions,
collectionPrefix,
schemaCacheTTL,
enableSingleSchemaCache,
} = options;
let { databaseAdapter } = options; let { databaseAdapter } = options;
if ( if (
(databaseOptions || (databaseOptions ||
@@ -165,10 +157,7 @@ export function getDatabaseController(
} else { } else {
databaseAdapter = loadAdapter(databaseAdapter); databaseAdapter = loadAdapter(databaseAdapter);
} }
return new DatabaseController( return new DatabaseController(databaseAdapter);
databaseAdapter,
new SchemaCache(cacheController, schemaCacheTTL, enableSingleSchemaCache)
);
} }
export function getHooksController( export function getHooksController(

View File

@@ -11,6 +11,7 @@ import * as defaultGraphQLQueries from './loaders/defaultGraphQLQueries';
import * as defaultGraphQLMutations from './loaders/defaultGraphQLMutations'; import * as defaultGraphQLMutations from './loaders/defaultGraphQLMutations';
import ParseGraphQLController, { ParseGraphQLConfig } from '../Controllers/ParseGraphQLController'; import ParseGraphQLController, { ParseGraphQLConfig } from '../Controllers/ParseGraphQLController';
import DatabaseController from '../Controllers/DatabaseController'; import DatabaseController from '../Controllers/DatabaseController';
import SchemaCache from '../Adapters/Cache/SchemaCache';
import { toGraphQLError } from './parseGraphQLUtils'; import { toGraphQLError } from './parseGraphQLUtils';
import * as schemaDirectives from './loaders/schemaDirectives'; import * as schemaDirectives from './loaders/schemaDirectives';
import * as schemaTypes from './loaders/schemaTypes'; import * as schemaTypes from './loaders/schemaTypes';
@@ -66,6 +67,7 @@ class ParseGraphQLSchema {
log: any; log: any;
appId: string; appId: string;
graphQLCustomTypeDefs: ?(string | GraphQLSchema | DocumentNode | GraphQLNamedType[]); graphQLCustomTypeDefs: ?(string | GraphQLSchema | DocumentNode | GraphQLNamedType[]);
schemaCache: any;
constructor( constructor(
params: { params: {
@@ -85,6 +87,7 @@ class ParseGraphQLSchema {
this.log = params.log || requiredParameter('You must provide a log instance!'); this.log = params.log || requiredParameter('You must provide a log instance!');
this.graphQLCustomTypeDefs = params.graphQLCustomTypeDefs; this.graphQLCustomTypeDefs = params.graphQLCustomTypeDefs;
this.appId = params.appId || requiredParameter('You must provide the appId!'); this.appId = params.appId || requiredParameter('You must provide the appId!');
this.schemaCache = SchemaCache;
} }
async load() { async load() {

View File

@@ -100,7 +100,7 @@ module.exports.ParseServerOptions = {
}, },
databaseOptions: { databaseOptions: {
env: 'PARSE_SERVER_DATABASE_OPTIONS', env: 'PARSE_SERVER_DATABASE_OPTIONS',
help: 'Options to pass to the mongodb client', help: 'Options to pass to the database client',
action: parsers.objectParser, action: parsers.objectParser,
}, },
databaseURI: { databaseURI: {
@@ -149,13 +149,6 @@ module.exports.ParseServerOptions = {
action: parsers.booleanParser, action: parsers.booleanParser,
default: false, default: false,
}, },
enableSingleSchemaCache: {
env: 'PARSE_SERVER_ENABLE_SINGLE_SCHEMA_CACHE',
help:
'Use a single schema cache shared across requests. Reduces number of queries made to _SCHEMA, defaults to false, i.e. unique schema cache per request.',
action: parsers.booleanParser,
default: false,
},
encryptionKey: { encryptionKey: {
env: 'PARSE_SERVER_ENCRYPTION_KEY', env: 'PARSE_SERVER_ENCRYPTION_KEY',
help: 'Key for encrypting your files', help: 'Key for encrypting your files',
@@ -366,13 +359,6 @@ module.exports.ParseServerOptions = {
action: parsers.booleanParser, action: parsers.booleanParser,
default: false, default: false,
}, },
schemaCacheTTL: {
env: 'PARSE_SERVER_SCHEMA_CACHE_TTL',
help:
'The TTL for caching the schema for optimizing read/write operations. You should put a long TTL when your DB is in production. default to 5000; set 0 to disable.',
action: parsers.numberParser('schemaCacheTTL'),
default: 5000,
},
security: { security: {
env: 'PARSE_SERVER_SECURITY', env: 'PARSE_SERVER_SECURITY',
help: 'The security options to identify and report weak security settings.', help: 'The security options to identify and report weak security settings.',
@@ -788,3 +774,12 @@ module.exports.FileUploadOptions = {
default: false, default: false,
}, },
}; };
module.exports.DatabaseOptions = {
enableSchemaHooks: {
env: 'PARSE_SERVER_DATABASE_ENABLE_SCHEMA_HOOKS',
help:
'Enables database hooks to update single schema cache. Set to true if using multiple Parse Servers instances connected to the same database.',
action: parsers.booleanParser,
default: false,
},
};

View File

@@ -18,7 +18,7 @@
* @property {String} collectionPrefix A collection prefix for the classes * @property {String} collectionPrefix A collection prefix for the classes
* @property {CustomPagesOptions} customPages custom pages for password validation and reset * @property {CustomPagesOptions} customPages custom pages for password validation and reset
* @property {Adapter<StorageAdapter>} databaseAdapter Adapter module for the database * @property {Adapter<StorageAdapter>} databaseAdapter Adapter module for the database
* @property {Any} databaseOptions Options to pass to the mongodb client * @property {DatabaseOptions} databaseOptions Options to pass to the database client
* @property {String} databaseURI The full URI to your database. Supported databases are mongodb or postgres. * @property {String} databaseURI The full URI to your database. Supported databases are mongodb or postgres.
* @property {Boolean} directAccess Replace HTTP Interface when using JS SDK in current node runtime, defaults to false. Caution, this is an experimental feature that may not be appropriate for production. * @property {Boolean} directAccess Replace HTTP Interface when using JS SDK in current node runtime, defaults to false. Caution, this is an experimental feature that may not be appropriate for production.
* @property {String} dotNetKey Key for Unity and .Net SDK * @property {String} dotNetKey Key for Unity and .Net SDK
@@ -27,7 +27,6 @@
* @property {Number} emailVerifyTokenValidityDuration Email verification token validity duration, in seconds * @property {Number} emailVerifyTokenValidityDuration Email verification token validity duration, in seconds
* @property {Boolean} enableAnonymousUsers Enable (or disable) anonymous users, defaults to true * @property {Boolean} enableAnonymousUsers Enable (or disable) anonymous users, defaults to true
* @property {Boolean} enableExpressErrorHandler Enables the default express error handler for all errors * @property {Boolean} enableExpressErrorHandler Enables the default express error handler for all errors
* @property {Boolean} enableSingleSchemaCache Use a single schema cache shared across requests. Reduces number of queries made to _SCHEMA, defaults to false, i.e. unique schema cache per request.
* @property {String} encryptionKey Key for encrypting your files * @property {String} encryptionKey Key for encrypting your files
* @property {Boolean} expireInactiveSessions Sets wether we should expire the inactive sessions, defaults to true * @property {Boolean} expireInactiveSessions Sets wether we should expire the inactive sessions, defaults to true
* @property {String} fileKey Key for your files * @property {String} fileKey Key for your files
@@ -67,7 +66,6 @@
* @property {String} restAPIKey Key for REST calls * @property {String} restAPIKey Key for REST calls
* @property {Boolean} revokeSessionOnPasswordReset When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions. * @property {Boolean} revokeSessionOnPasswordReset When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions.
* @property {Boolean} scheduledPush Configuration for push scheduling, defaults to false. * @property {Boolean} scheduledPush Configuration for push scheduling, defaults to false.
* @property {Number} schemaCacheTTL The TTL for caching the schema for optimizing read/write operations. You should put a long TTL when your DB is in production. default to 5000; set 0 to disable.
* @property {SecurityOptions} security The security options to identify and report weak security settings. * @property {SecurityOptions} security The security options to identify and report weak security settings.
* @property {Function} serverCloseComplete Callback when server has closed * @property {Function} serverCloseComplete Callback when server has closed
* @property {Function} serverStartComplete Callback when server has started * @property {Function} serverStartComplete Callback when server has started
@@ -190,3 +188,8 @@
* @property {Boolean} enableForAuthenticatedUser Is true if file upload should be allowed for authenticated users. * @property {Boolean} enableForAuthenticatedUser Is true if file upload should be allowed for authenticated users.
* @property {Boolean} enableForPublic Is true if file upload should be allowed for anyone, regardless of user authentication. * @property {Boolean} enableForPublic Is true if file upload should be allowed for anyone, regardless of user authentication.
*/ */
/**
* @interface DatabaseOptions
* @property {Boolean} enableSchemaHooks Enables database hooks to update single schema cache. Set to true if using multiple Parse Servers instances connected to the same database.
*/

View File

@@ -63,8 +63,9 @@ export interface ParseServerOptions {
/* The full URI to your database. Supported databases are mongodb or postgres. /* The full URI to your database. Supported databases are mongodb or postgres.
:DEFAULT: mongodb://localhost:27017/parse */ :DEFAULT: mongodb://localhost:27017/parse */
databaseURI: string; databaseURI: string;
/* Options to pass to the mongodb client */ /* Options to pass to the database client
databaseOptions: ?any; :ENV: PARSE_SERVER_DATABASE_OPTIONS */
databaseOptions: ?DatabaseOptions;
/* Adapter module for the database */ /* Adapter module for the database */
databaseAdapter: ?Adapter<StorageAdapter>; databaseAdapter: ?Adapter<StorageAdapter>;
/* Full path to your cloud code main.js */ /* Full path to your cloud code main.js */
@@ -158,9 +159,6 @@ export interface ParseServerOptions {
/* When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions. /* When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions.
:DEFAULT: true */ :DEFAULT: true */
revokeSessionOnPasswordReset: ?boolean; revokeSessionOnPasswordReset: ?boolean;
/* The TTL for caching the schema for optimizing read/write operations. You should put a long TTL when your DB is in production. default to 5000; set 0 to disable.
:DEFAULT: 5000 */
schemaCacheTTL: ?number;
/* Sets the TTL for the in memory cache (in ms), defaults to 5000 (5 seconds) /* Sets the TTL for the in memory cache (in ms), defaults to 5000 (5 seconds)
:DEFAULT: 5000 */ :DEFAULT: 5000 */
cacheTTL: ?number; cacheTTL: ?number;
@@ -171,9 +169,6 @@ export interface ParseServerOptions {
:ENV: PARSE_SERVER_ENABLE_EXPERIMENTAL_DIRECT_ACCESS :ENV: PARSE_SERVER_ENABLE_EXPERIMENTAL_DIRECT_ACCESS
:DEFAULT: false */ :DEFAULT: false */
directAccess: ?boolean; directAccess: ?boolean;
/* Use a single schema cache shared across requests. Reduces number of queries made to _SCHEMA, defaults to false, i.e. unique schema cache per request.
:DEFAULT: false */
enableSingleSchemaCache: ?boolean;
/* Enables the default express error handler for all errors /* Enables the default express error handler for all errors
:DEFAULT: false */ :DEFAULT: false */
enableExpressErrorHandler: ?boolean; enableExpressErrorHandler: ?boolean;
@@ -416,3 +411,9 @@ export interface FileUploadOptions {
:DEFAULT: false */ :DEFAULT: false */
enableForPublic: ?boolean; enableForPublic: ?boolean;
} }
export interface DatabaseOptions {
/* Enables database hooks to update single schema cache. Set to true if using multiple Parse Servers instances connected to the same database.
:DEFAULT: false */
enableSchemaHooks: ?boolean;
}

View File

@@ -150,7 +150,6 @@ function makeExpressHandler(appId, promiseHandler) {
promiseHandler(req) promiseHandler(req)
.then( .then(
result => { result => {
clearSchemaCache(req);
if (!result.response && !result.location && !result.text) { if (!result.response && !result.location && !result.text) {
log.error('the handler did not include a "response" or a "location" field'); log.error('the handler did not include a "response" or a "location" field');
throw 'control should not get here'; throw 'control should not get here';
@@ -184,17 +183,14 @@ function makeExpressHandler(appId, promiseHandler) {
res.json(result.response); res.json(result.response);
}, },
error => { error => {
clearSchemaCache(req);
next(error); next(error);
} }
) )
.catch(e => { .catch(e => {
clearSchemaCache(req);
log.error(`Error generating response. ${inspect(e)}`, { error: e }); log.error(`Error generating response. ${inspect(e)}`, { error: e });
next(e); next(e);
}); });
} catch (e) { } catch (e) {
clearSchemaCache(req);
log.error(`Error handling request: ${inspect(e)}`, { error: e }); log.error(`Error handling request: ${inspect(e)}`, { error: e });
next(e); next(e);
} }
@@ -212,9 +208,3 @@ function maskSensitiveUrl(req) {
} }
return maskUrl; return maskUrl;
} }
function clearSchemaCache(req) {
if (req.config && !req.config.enableSingleSchemaCache) {
req.config.database.schemaCache.clear();
}
}