diff --git a/CHANGELOG.md b/CHANGELOG.md index b6657365ac..a7c333d618 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -89,6 +89,9 @@ ___ ## Unreleased (Master Branch) [Full Changelog](https://github.com/parse-community/parse-server/compare/4.5.0...master) ### 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) ### 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) diff --git a/resources/buildConfigDefinitions.js b/resources/buildConfigDefinitions.js index 9a03dbf353..ef4994af47 100644 --- a/resources/buildConfigDefinitions.js +++ b/resources/buildConfigDefinitions.js @@ -53,6 +53,7 @@ function getENVPrefix(iface) { 'PasswordPolicyOptions' : 'PARSE_SERVER_PASSWORD_POLICY_', 'FileUploadOptions' : 'PARSE_SERVER_FILE_UPLOAD_', 'SecurityOptions': 'PARSE_SERVER_SECURITY_', + 'DatabaseOptions': 'PARSE_SERVER_DATABASE_' } if (options[iface.id.name]) { return options[iface.id.name] @@ -168,7 +169,7 @@ function parseDefaultValue(elt, value, t) { if (type == 'NumberOrBoolean') { 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)) { const object = parsers.objectParser(value); const props = Object.keys(object).map((key) => { diff --git a/spec/EnableSingleSchemaCache.spec.js b/spec/EnableSingleSchemaCache.spec.js deleted file mode 100644 index 45873c52f6..0000000000 --- a/spec/EnableSingleSchemaCache.spec.js +++ /dev/null @@ -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'); -}; diff --git a/spec/LogsRouter.spec.js b/spec/LogsRouter.spec.js index 7a70ef9fd5..eb119fe56c 100644 --- a/spec/LogsRouter.spec.js +++ b/spec/LogsRouter.spec.js @@ -8,7 +8,9 @@ const WinstonLoggerAdapter = require('../lib/Adapters/Logger/WinstonLoggerAdapte 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 => { // Make mock request const request = { diff --git a/spec/MongoStorageAdapter.spec.js b/spec/MongoStorageAdapter.spec.js index 9cd4094698..f6d2866417 100644 --- a/spec/MongoStorageAdapter.spec.js +++ b/spec/MongoStorageAdapter.spec.js @@ -18,6 +18,7 @@ const fakeClient = { describe_only_db('mongo')('MongoStorageAdapter', () => { beforeEach(done => { new MongoStorageAdapter({ uri: databaseURI }).deleteAllClasses().then(done, fail); + Config.get(Parse.applicationId).schemaCache.clear(); }); it('auto-escapes symbols in auth information', () => { @@ -314,6 +315,8 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { await user.signUp(); const database = Config.get(Parse.applicationId).database; + await database.adapter.dropAllIndexes('_User'); + const preIndexPlan = await database.find( '_User', { 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); + }); + }); } }); diff --git a/spec/ParseGraphQLController.spec.js b/spec/ParseGraphQLController.spec.js index 7a60e48ba5..9eed8f52be 100644 --- a/spec/ParseGraphQLController.spec.js +++ b/spec/ParseGraphQLController.spec.js @@ -30,9 +30,7 @@ describe('ParseGraphQLController', () => { beforeEach(async () => { if (!parseServer) { - parseServer = await global.reconfigureServer({ - schemaCacheTTL: 100, - }); + parseServer = await global.reconfigureServer(); databaseController = parseServer.config.databaseController; cacheController = parseServer.config.cacheController; diff --git a/spec/ParseGraphQLSchema.spec.js b/spec/ParseGraphQLSchema.spec.js index ee815588a9..67472d6e91 100644 --- a/spec/ParseGraphQLSchema.spec.js +++ b/spec/ParseGraphQLSchema.spec.js @@ -10,9 +10,7 @@ describe('ParseGraphQLSchema', () => { const appId = 'test'; beforeEach(async () => { - parseServer = await global.reconfigureServer({ - schemaCacheTTL: 100, - }); + parseServer = await global.reconfigureServer(); databaseController = parseServer.config.databaseController; parseGraphQLController = parseServer.config.parseGraphQLController; parseGraphQLSchema = new ParseGraphQLSchema({ @@ -68,7 +66,7 @@ describe('ParseGraphQLSchema', () => { const graphQLSubscriptions = parseGraphQLSchema.graphQLSubscriptions; const newClassObject = new Parse.Object('NewClass'); await newClassObject.save(); - await databaseController.schemaCache.clear(); + await parseServer.config.schemaCache.clear(); await new Promise(resolve => setTimeout(resolve, 200)); await parseGraphQLSchema.load(); expect(parseClasses).not.toBe(parseGraphQLSchema.parseClasses); @@ -426,14 +424,14 @@ describe('ParseGraphQLSchema', () => { log: defaultLogger, appId, }); - await parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLSchema.schemaCache.clear(); const schema1 = await parseGraphQLSchema.load(); const types1 = parseGraphQLSchema.graphQLTypes; const queries1 = parseGraphQLSchema.graphQLQueries; const mutations1 = parseGraphQLSchema.graphQLMutations; const user = new Parse.Object('User'); await user.save(); - await parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLSchema.schemaCache.clear(); const schema2 = await parseGraphQLSchema.load(); const types2 = parseGraphQLSchema.graphQLTypes; const queries2 = parseGraphQLSchema.graphQLQueries; @@ -456,14 +454,14 @@ describe('ParseGraphQLSchema', () => { }); const car1 = new Parse.Object('Car'); await car1.save(); - await parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLSchema.schemaCache.clear(); const schema1 = await parseGraphQLSchema.load(); const types1 = parseGraphQLSchema.graphQLTypes; const queries1 = parseGraphQLSchema.graphQLQueries; const mutations1 = parseGraphQLSchema.graphQLMutations; const car2 = new Parse.Object('car'); await car2.save(); - await parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLSchema.schemaCache.clear(); const schema2 = await parseGraphQLSchema.load(); const types2 = parseGraphQLSchema.graphQLTypes; const queries2 = parseGraphQLSchema.graphQLQueries; @@ -486,13 +484,13 @@ describe('ParseGraphQLSchema', () => { }); const car = new Parse.Object('Car'); await car.save(); - await parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLSchema.schemaCache.clear(); const schema1 = await parseGraphQLSchema.load(); const queries1 = parseGraphQLSchema.graphQLQueries; const mutations1 = parseGraphQLSchema.graphQLMutations; const cars = new Parse.Object('cars'); await cars.save(); - await parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLSchema.schemaCache.clear(); const schema2 = await parseGraphQLSchema.load(); const queries2 = parseGraphQLSchema.graphQLQueries; const mutations2 = parseGraphQLSchema.graphQLMutations; @@ -532,7 +530,7 @@ describe('ParseGraphQLSchema', () => { await data.save(); - await parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLSchema.schemaCache.clear(); await parseGraphQLSchema.load(); const queries1 = parseGraphQLSchema.graphQLQueries; @@ -569,7 +567,7 @@ describe('ParseGraphQLSchema', () => { await data.save(); - await parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLSchema.schemaCache.clear(); await parseGraphQLSchema.load(); const mutations = parseGraphQLSchema.graphQLMutations; diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 7afd8a549d..2e06e60c2d 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -538,7 +538,7 @@ describe('ParseGraphQLServer', () => { const resetGraphQLCache = async () => { await Promise.all([ 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'); await obj.save(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const createObjectInputFields = ( await apolloClient.query({ @@ -1117,7 +1117,7 @@ describe('ParseGraphQLServer', () => { const obj = new Parse.Object('SomeClass'); await obj.save(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const createObjectPayloadFields = ( await apolloClient.query({ @@ -1142,7 +1142,7 @@ describe('ParseGraphQLServer', () => { const obj = new Parse.Object('SomeClass'); await obj.save(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const createObjectInputFields = ( await apolloClient.query({ @@ -1167,7 +1167,7 @@ describe('ParseGraphQLServer', () => { const obj = new Parse.Object('SomeClass'); await obj.save(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const createObjectPayloadFields = ( await apolloClient.query({ @@ -1192,7 +1192,7 @@ describe('ParseGraphQLServer', () => { const obj = new Parse.Object('SomeClass'); await obj.save(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const createObjectInputFields = ( await apolloClient.query({ @@ -1217,7 +1217,7 @@ describe('ParseGraphQLServer', () => { const obj = new Parse.Object('SomeClass'); await obj.save(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const createObjectPayloadFields = ( await apolloClient.query({ @@ -1339,7 +1339,7 @@ describe('ParseGraphQLServer', () => { const resetGraphQLCache = async () => { await Promise.all([ parseGraphQLServer.parseGraphQLController.cacheController.graphQL.clear(), - parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(), + parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(), ]); }; @@ -3921,7 +3921,7 @@ describe('ParseGraphQLServer', () => { obj.set('someField', 'someValue'); await obj.save(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const result = ( await apolloClient.query({ @@ -3964,7 +3964,7 @@ describe('ParseGraphQLServer', () => { obj3.set('manyRelations', [obj1, obj2]); await obj3.save(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const result = ( await apolloClient.query({ @@ -4039,7 +4039,7 @@ describe('ParseGraphQLServer', () => { obj1.set('country', obj4); await obj1.save(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const result = ( await apolloClient.query({ @@ -4130,7 +4130,7 @@ describe('ParseGraphQLServer', () => { it('should respect level permissions', async () => { await prepareData(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); async function getObject(className, id, headers) { const alias = className.charAt(0).toLowerCase() + className.slice(1); @@ -4260,7 +4260,7 @@ describe('ParseGraphQLServer', () => { it('should support keys argument', async () => { await prepareData(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const result1 = await apolloClient.query({ query: gql` @@ -4310,7 +4310,7 @@ describe('ParseGraphQLServer', () => { it('should support include argument', async () => { await prepareData(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const result1 = await apolloClient.query({ query: gql` @@ -4358,7 +4358,7 @@ describe('ParseGraphQLServer', () => { it('should respect protectedFields', async done => { await prepareData(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const className = 'GraphQLClass'; @@ -4439,7 +4439,7 @@ describe('ParseGraphQLServer', () => { try { await prepareData(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const databaseAdapter = parseServer.config.databaseController.adapter; spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); @@ -4486,7 +4486,7 @@ describe('ParseGraphQLServer', () => { it('should support readPreference argument', async () => { await prepareData(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const databaseAdapter = parseServer.config.databaseController.adapter; spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); @@ -4530,7 +4530,7 @@ describe('ParseGraphQLServer', () => { it('should support includeReadPreference argument', async () => { await prepareData(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const databaseAdapter = parseServer.config.databaseController.adapter; spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); @@ -4585,7 +4585,7 @@ describe('ParseGraphQLServer', () => { obj2.set('someField', 'someValue1'); await obj2.save(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const result = await apolloClient.query({ query: gql` @@ -4618,7 +4618,7 @@ describe('ParseGraphQLServer', () => { it('should respect level permissions', async () => { await prepareData(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); async function findObjects(className, headers) { const graphqlClassName = pluralize( @@ -4724,7 +4724,7 @@ describe('ParseGraphQLServer', () => { it('should support where argument using class specific query', async () => { await prepareData(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const result = await apolloClient.query({ query: gql` @@ -4776,7 +4776,7 @@ describe('ParseGraphQLServer', () => { it('should support in pointer operator using class specific query', async () => { await prepareData(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const result = await apolloClient.query({ query: gql` @@ -4816,7 +4816,7 @@ describe('ParseGraphQLServer', () => { it('should support OR operation', async () => { await prepareData(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const result = await apolloClient.query({ query: gql` @@ -4856,7 +4856,7 @@ describe('ParseGraphQLServer', () => { obj.set('field2', 'It rocks!'); await obj.save(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const result = await apolloClient.query({ query: gql` @@ -4914,7 +4914,7 @@ describe('ParseGraphQLServer', () => { city2.set('name', 'city2'); await city2.save(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const { data: { @@ -4970,7 +4970,7 @@ describe('ParseGraphQLServer', () => { } await Promise.all(promises); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const result = await apolloClient.query({ query: gql` @@ -5024,7 +5024,7 @@ describe('ParseGraphQLServer', () => { } await Promise.all(promises); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const find = async ({ skip, after, first, before, last } = {}) => { return await apolloClient.query({ @@ -5152,7 +5152,7 @@ describe('ParseGraphQLServer', () => { it('should support count', async () => { await prepareData(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const where = { someField: { @@ -5207,7 +5207,7 @@ describe('ParseGraphQLServer', () => { it('should only count', async () => { await prepareData(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const where = { someField: { @@ -5265,7 +5265,7 @@ describe('ParseGraphQLServer', () => { } await Promise.all(promises); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const result = await apolloClient.query({ query: gql` @@ -5297,7 +5297,7 @@ describe('ParseGraphQLServer', () => { it('should support keys argument', async () => { await prepareData(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const result1 = await apolloClient.query({ query: gql` @@ -5359,7 +5359,7 @@ describe('ParseGraphQLServer', () => { it('should support include argument', async () => { await prepareData(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const where = { id: { @@ -5422,7 +5422,7 @@ describe('ParseGraphQLServer', () => { it('should read from primary by default', async () => { await prepareData(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const databaseAdapter = parseServer.config.databaseController.adapter; spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); @@ -5467,7 +5467,7 @@ describe('ParseGraphQLServer', () => { it('should support readPreference argument', async () => { await prepareData(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const databaseAdapter = parseServer.config.databaseController.adapter; spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); @@ -5512,7 +5512,7 @@ describe('ParseGraphQLServer', () => { it('should support includeReadPreference argument', async () => { await prepareData(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const databaseAdapter = parseServer.config.databaseController.adapter; spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); @@ -5560,7 +5560,7 @@ describe('ParseGraphQLServer', () => { try { await prepareData(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const databaseAdapter = parseServer.config.databaseController.adapter; spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); @@ -5714,7 +5714,7 @@ describe('ParseGraphQLServer', () => { customerSchema.addString('someField'); await customerSchema.save(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const result = await apolloClient.mutate({ mutation: gql` @@ -5757,7 +5757,7 @@ describe('ParseGraphQLServer', () => { it('should respect level permissions', async () => { await prepareData(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); async function createObject(className, headers) { const getClassName = className.charAt(0).toLowerCase() + className.slice(1); @@ -5837,7 +5837,7 @@ describe('ParseGraphQLServer', () => { obj.set('someField2', 'someField2Value1'); await obj.save(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const result = await apolloClient.mutate({ mutation: gql` @@ -5880,7 +5880,7 @@ describe('ParseGraphQLServer', () => { obj.set('someField2', 'someField2Value1'); await obj.save(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const result = await apolloClient.mutate({ mutation: gql` @@ -5912,7 +5912,7 @@ describe('ParseGraphQLServer', () => { it('should respect level permissions', async () => { await prepareData(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); async function updateObject(className, id, fields, headers) { return await apolloClient.mutate({ @@ -6107,7 +6107,7 @@ describe('ParseGraphQLServer', () => { it('should respect level permissions with specific class mutation', async () => { await prepareData(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); function updateObject(className, id, fields, headers) { const mutationName = className.charAt(0).toLowerCase() + className.slice(1); @@ -6327,7 +6327,7 @@ describe('ParseGraphQLServer', () => { obj.set('someField2', 'someField2Value1'); await obj.save(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const result = await apolloClient.mutate({ mutation: gql` @@ -6364,7 +6364,7 @@ describe('ParseGraphQLServer', () => { it('should respect level permissions', async () => { await prepareData(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); function deleteObject(className, id, headers) { 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 () => { await prepareData(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); function deleteObject(className, id, headers) { const mutationName = className.charAt(0).toLowerCase() + className.slice(1); @@ -6666,7 +6666,7 @@ describe('ParseGraphQLServer', () => { user.set('userFoo', foo); await user.signUp(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const session = await Parse.Session.current(); const result = await apolloClient.query({ @@ -6717,7 +6717,7 @@ describe('ParseGraphQLServer', () => { user.set('userFoo', foo); await user.signUp(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const session = await Parse.Session.current(); const result = await apolloClient.query({ @@ -6758,7 +6758,7 @@ describe('ParseGraphQLServer', () => { userSchema.addPointer('aPointer', '_User'); await userSchema.update(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const result = await apolloClient.mutate({ mutation: gql` mutation SignUp($input: SignUpInput!) { @@ -6819,7 +6819,7 @@ describe('ParseGraphQLServer', () => { userSchema.addString('someField'); userSchema.addPointer('aPointer', '_User'); await userSchema.update(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const result = await apolloClient.mutate({ mutation: gql` mutation LogInWith($input: LogInWithInput!) { @@ -6877,7 +6877,7 @@ describe('ParseGraphQLServer', () => { user.set('someField', 'someValue'); await user.signUp(); await Parse.User.logOut(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const result = await apolloClient.mutate({ mutation: gql` mutation LogInUser($input: LogInInput!) { @@ -7120,7 +7120,7 @@ describe('ParseGraphQLServer', () => { const car = new Parse.Object('Car'); await car.save(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); try { 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(); 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({ 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(); 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(); 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(); expect(schema.fields.someField.type).toEqual('Date'); @@ -7827,7 +7827,7 @@ describe('ParseGraphQLServer', () => { const role2 = new Parse.Role('aRole2', roleACL); await role2.save(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const gqlUser = ( await apolloClient.query({ @@ -8013,7 +8013,7 @@ describe('ParseGraphQLServer', () => { company2.set('name', 'imACompany2'); await company2.save(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const { data: { @@ -8058,7 +8058,7 @@ describe('ParseGraphQLServer', () => { country.set('company', company); await country.save(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const { data: { @@ -8109,7 +8109,7 @@ describe('ParseGraphQLServer', () => { company2.set('name', 'imACompany2'); await company2.save(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const { data: { @@ -8154,7 +8154,7 @@ describe('ParseGraphQLServer', () => { country.set('company', company); await country.save(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const { data: { @@ -8201,7 +8201,7 @@ describe('ParseGraphQLServer', () => { country.relation('companies').add(company); await country.save(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const { data: { @@ -8269,7 +8269,7 @@ describe('ParseGraphQLServer', () => { country.relation('companies').add(company); await country.save(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const { data: { @@ -8362,7 +8362,7 @@ describe('ParseGraphQLServer', () => { country.relation('companies').add(company1); await country.save(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const { data: { @@ -8421,7 +8421,7 @@ describe('ParseGraphQLServer', () => { country.relation('companies').add(company); await country.save(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const { data: { @@ -8489,7 +8489,7 @@ describe('ParseGraphQLServer', () => { country.relation('companies').add([company1, company2]); await country.save(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); // Without where const { @@ -8589,7 +8589,7 @@ describe('ParseGraphQLServer', () => { country3.set('president', president); await country3.save(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); let { data: { @@ -8856,7 +8856,7 @@ describe('ParseGraphQLServer', () => { }, }); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const body2 = new FormData(); 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(); 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({ 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(); expect(schema.fields.someArrayField.type).toEqual('Array'); @@ -9342,7 +9342,7 @@ describe('ParseGraphQLServer', () => { const obj = new Parse.Object('SomeClass'); await obj.save(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const getResult = await apolloClient.query({ query: gql` @@ -9391,7 +9391,7 @@ describe('ParseGraphQLServer', () => { }, }); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const createResult = await apolloClient.mutate({ 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(); 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(); 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(); expect(schema.fields.somePolygonField.type).toEqual('Polygon'); @@ -9830,7 +9830,7 @@ describe('ParseGraphQLServer', () => { }); await someClass.save(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const schema = await new Parse.Schema('SomeClass').get(); expect(schema.fields.someField.type).toEqual('Bytes'); @@ -9930,7 +9930,7 @@ describe('ParseGraphQLServer', () => { user.setPassword('user1'); await user.signUp(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const getResult = await apolloClient.query({ query: gql` @@ -9954,7 +9954,7 @@ describe('ParseGraphQLServer', () => { deviceType: 'foo', }); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const getResult = await apolloClient.query({ query: gql` @@ -9978,7 +9978,7 @@ describe('ParseGraphQLServer', () => { const role = new Parse.Role('MyRole', roleACL); await role.save(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const getResult = await apolloClient.query({ query: gql` @@ -10002,7 +10002,7 @@ describe('ParseGraphQLServer', () => { user.setPassword('user1'); await user.signUp(); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const session = await Parse.Session.current(); const getResult = await apolloClient.query({ @@ -10041,7 +10041,7 @@ describe('ParseGraphQLServer', () => { { useMasterKey: true } ); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const getResult = await apolloClient.query({ query: gql` @@ -10170,7 +10170,7 @@ describe('ParseGraphQLServer', () => { await Promise.all([ parseGraphQLServer.parseGraphQLController.cacheController.graphQL.clear(), - parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(), + parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(), ]); await expectAsync( @@ -10311,7 +10311,7 @@ describe('ParseGraphQLServer', () => { it('can resolve a custom query with auto type return', async () => { const obj = new Parse.Object('SomeClass'); await obj.save({ name: 'aname', type: 'robot' }); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const result = await apolloClient.query({ variables: { id: obj.id }, query: gql` @@ -10334,7 +10334,7 @@ describe('ParseGraphQLServer', () => { it('can resolve a custom extend type', async () => { const obj = new Parse.Object('SomeClass'); await obj.save({ name: 'aname', type: 'robot' }); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const result = await apolloClient.query({ variables: { id: obj.id }, query: gql` diff --git a/spec/ParseQuery.hint.spec.js b/spec/ParseQuery.hint.spec.js index 164fff7880..2685137801 100644 --- a/spec/ParseQuery.hint.spec.js +++ b/spec/ParseQuery.hint.spec.js @@ -24,7 +24,6 @@ describe_only_db('mongo')('Parse.Query hint', () => { }); afterEach(async () => { - await config.database.schemaCache.clear(); await TestUtils.destroyAllDataPermanently(false); }); diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index be636e53cb..91aeb4920a 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -247,6 +247,7 @@ describe('Parse.User testing', () => { await adapter.connect(); await adapter.database.dropDatabase(); delete adapter.connectionPromise; + Config.get(Parse.applicationId).schemaCache.clear(); const user = new Parse.User(); await user.signUp({ diff --git a/spec/PointerPermissions.spec.js b/spec/PointerPermissions.spec.js index fa84774919..e01da055be 100644 --- a/spec/PointerPermissions.spec.js +++ b/spec/PointerPermissions.spec.js @@ -3,7 +3,7 @@ const Config = require('../lib/Config'); describe('Pointer Permissions', () => { beforeEach(() => { - Config.get(Parse.applicationId).database.schemaCache.clear(); + Config.get(Parse.applicationId).schemaCache.clear(); }); describe('using single user-pointers', () => { @@ -2020,7 +2020,7 @@ describe('Pointer Permissions', () => { let obj2; 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')]); @@ -2442,7 +2442,7 @@ describe('Pointer Permissions', () => { let objNobody; async function initialize() { - await Config.get(Parse.applicationId).database.schemaCache.clear(); + await Config.get(Parse.applicationId).schemaCache.clear(); [user1, user2, user3] = await Promise.all([ createUser('user1'), @@ -2919,7 +2919,7 @@ describe('Pointer Permissions', () => { let obj2; 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')]); @@ -3033,7 +3033,7 @@ describe('Pointer Permissions', () => { * Clear cache, create user and object, login user */ async function initialize() { - await Config.get(Parse.applicationId).database.schemaCache.clear(); + await Config.get(Parse.applicationId).schemaCache.clear(); user1 = await createUser('user1'); user1 = await logIn(user1); diff --git a/spec/PostgresStorageAdapter.spec.js b/spec/PostgresStorageAdapter.spec.js index 8c372362b5..b042206db2 100644 --- a/spec/PostgresStorageAdapter.spec.js +++ b/spec/PostgresStorageAdapter.spec.js @@ -26,6 +26,8 @@ describe_only_db('postgres')('PostgresStorageAdapter', () => { it('schemaUpgrade, upgrade the database schema when schema changes', async done => { await adapter.deleteAllClasses(); + const config = Config.get('test'); + config.schemaCache.clear(); await adapter.performInitialization({ VolatileClassesSchemas: [] }); const client = adapter._client; const className = '_PushStatus'; @@ -232,13 +234,19 @@ describe_only_db('postgres')('PostgresStorageAdapter', () => { }); 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 user = new Parse.User(); user.set('username', 'Elmer'); user.set('password', 'Fudd'); 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 const client = adapter._client; @@ -287,12 +295,19 @@ describe_only_db('postgres')('PostgresStorageAdapter', () => { }); 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 user = new Parse.User(); user.set('username', 'Tweety'); user.set('password', 'Bird'); await user.signUp(); - const database = Config.get(Parse.applicationId).database; + const fieldToSearch = 'username'; //Create index before data is inserted 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', () => { diff --git a/spec/ProtectedFields.spec.js b/spec/ProtectedFields.spec.js index c3374923db..53673e8e02 100644 --- a/spec/ProtectedFields.spec.js +++ b/spec/ProtectedFields.spec.js @@ -135,7 +135,7 @@ describe('ProtectedFields', function () { describe('using the pointer-permission variant', () => { let user1, user2; beforeEach(async () => { - Config.get(Parse.applicationId).database.schemaCache.clear(); + Config.get(Parse.applicationId).schemaCache.clear(); user1 = await Parse.User.signUp('user1', 'password'); user2 = await Parse.User.signUp('user2', 'password'); await Parse.User.logOut(); @@ -752,7 +752,7 @@ describe('ProtectedFields', function () { let object; async function initialize() { - await Config.get(Parse.applicationId).database.schemaCache.clear(); + await Config.get(Parse.applicationId).schemaCache.clear(); object = new Parse.Object(className); @@ -815,7 +815,7 @@ describe('ProtectedFields', function () { let obj1; async function initialize() { - await Config.get(Parse.applicationId).database.schemaCache.clear(); + await Config.get(Parse.applicationId).schemaCache.clear(); obj1 = new Parse.Object(className); @@ -924,7 +924,7 @@ describe('ProtectedFields', function () { let obj2; async function initialize() { - await Config.get(Parse.applicationId).database.schemaCache.clear(); + await Config.get(Parse.applicationId).schemaCache.clear(); await Parse.User.logOut(); @@ -1125,7 +1125,7 @@ describe('ProtectedFields', function () { let obj2; 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')]); @@ -1477,7 +1477,7 @@ describe('ProtectedFields', function () { * Clear cache, create user and object, login user and setup rest headers with token */ async function initialize() { - await Config.get(Parse.applicationId).database.schemaCache.clear(); + await Config.get(Parse.applicationId).schemaCache.clear(); user1 = await createUser('user1'); user1 = await logIn(user1); diff --git a/spec/ReadPreferenceOption.spec.js b/spec/ReadPreferenceOption.spec.js index d78aa92de9..f2bc328d99 100644 --- a/spec/ReadPreferenceOption.spec.js +++ b/spec/ReadPreferenceOption.spec.js @@ -7,7 +7,7 @@ const Config = require('../lib/Config'); function waitForReplication() { return new Promise(function (resolve) { - setTimeout(resolve, 300); + setTimeout(resolve, 1000); }); } diff --git a/spec/RedisCacheAdapter.spec.js b/spec/RedisCacheAdapter.spec.js index c980e85b7e..952d23eb0d 100644 --- a/spec/RedisCacheAdapter.spec.js +++ b/spec/RedisCacheAdapter.spec.js @@ -1,5 +1,4 @@ const RedisCacheAdapter = require('../lib/Adapters/Cache/RedisCacheAdapter').default; -const Config = require('../lib/Config'); function wait(sleep) { return new Promise(function (resolve) { @@ -168,356 +167,3 @@ describe_only(() => { .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); - }); -}); diff --git a/spec/Schema.spec.js b/spec/Schema.spec.js index 21378c1100..0975260a4b 100644 --- a/spec/Schema.spec.js +++ b/spec/Schema.spec.js @@ -24,10 +24,6 @@ describe('SchemaController', () => { config = Config.get('test'); }); - afterEach(async () => { - await config.database.schemaCache.clear(); - }); - it('can validate one object', done => { config.database .loadSchema() @@ -1349,17 +1345,6 @@ describe('SchemaController', () => { .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 () => { const schema = await config.database.loadSchema(); const result = await schema.enforceFieldExists('NewClass', 'fieldName', null); diff --git a/spec/SchemaCache.spec.js b/spec/SchemaCache.spec.js deleted file mode 100644 index e62ac8ab71..0000000000 --- a/spec/SchemaCache.spec.js +++ /dev/null @@ -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(); - }); -}); diff --git a/spec/SchemaPerformance.spec.js b/spec/SchemaPerformance.spec.js new file mode 100644 index 0000000000..21e97b0d43 --- /dev/null +++ b/spec/SchemaPerformance.spec.js @@ -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); + }); +}); diff --git a/spec/WinstonLoggerAdapter.spec.js b/spec/WinstonLoggerAdapter.spec.js index ca18be3739..4ceff47d5f 100644 --- a/spec/WinstonLoggerAdapter.spec.js +++ b/spec/WinstonLoggerAdapter.spec.js @@ -4,7 +4,9 @@ const WinstonLoggerAdapter = require('../lib/Adapters/Logger/WinstonLoggerAdapte .WinstonLoggerAdapter; 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 => { const winstonLoggerAdapter = new WinstonLoggerAdapter(); 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 => { const winstonLoggerAdapter = new WinstonLoggerAdapter(); 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 => { reconfigureServer({ verbose: true }) .then(() => createTestUser()) diff --git a/spec/helper.js b/spec/helper.js index 8ee874a93f..8d9a23f134 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -2,6 +2,7 @@ const semver = require('semver'); const CurrentSpecReporter = require('./support/CurrentSpecReporter.js'); const { SpecReporter } = require('jasmine-spec-reporter'); +const SchemaCache = require('../lib/Adapters/Cache/SchemaCache').default; // Sets up a Parse API server for testing. jasmine.DEFAULT_TIMEOUT_INTERVAL = process.env.PARSE_SERVER_TEST_TIMEOUT || 10000; @@ -206,6 +207,7 @@ afterEach(function (done) { } destroyAliveConnections(); await TestUtils.destroyAllDataPermanently(true); + SchemaCache.clear(); if (didChangeConfiguration) { await reconfigureServer(); } else { diff --git a/spec/index.spec.js b/spec/index.spec.js index 73be17cb51..5ab05bd21a 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -70,6 +70,8 @@ describe('server', () => { }, }), }).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 request({ method: 'POST', diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index e535b07a07..15dc9e111e 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -145,10 +145,6 @@ describe('schemas', () => { config = Config.get('test'); }); - afterEach(async () => { - await config.database.schemaCache.clear(); - }); - it('requires the master key to get all schemas', done => { request({ url: 'http://localhost:8378/1/schemas', diff --git a/spec/support/dev.js b/spec/support/dev.js index 93f73aa5a5..3415387c14 100644 --- a/spec/support/dev.js +++ b/spec/support/dev.js @@ -4,12 +4,9 @@ const Parse = require('parse/node'); const className = 'AnObject'; const defaultRoleName = 'tester'; -let schemaCache; - module.exports = { /* AnObject */ className, - schemaCache, /** * Creates and returns new user. diff --git a/src/Adapters/Auth/instagram.js b/src/Adapters/Auth/instagram.js index 0b493a6945..521796de63 100644 --- a/src/Adapters/Auth/instagram.js +++ b/src/Adapters/Auth/instagram.js @@ -8,7 +8,7 @@ function validateAuthData(authData) { const apiURL = authData.apiURL || defaultURL; const path = `${apiURL}me?fields=id&access_token=${authData.access_token}`; 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) { return; } diff --git a/src/Adapters/Cache/SchemaCache.js b/src/Adapters/Cache/SchemaCache.js new file mode 100644 index 0000000000..f55edf0635 --- /dev/null +++ b/src/Adapters/Cache/SchemaCache.js @@ -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; + }, +}; diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 94c2ca4039..2b5eaa0f09 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -113,12 +113,15 @@ export class MongoStorageAdapter implements StorageAdapter { _uri: string; _collectionPrefix: string; _mongoOptions: Object; + _onchange: any; + _stream: any; // Public connectionPromise: ?Promise; database: any; client: MongoClient; _maxTimeMS: ?number; canSortOnJoinTables: boolean; + enableSchemaHooks: boolean; constructor({ uri = defaults.DefaultMongoURI, collectionPrefix = '', mongoOptions = {} }: any) { this._uri = uri; @@ -126,13 +129,20 @@ export class MongoStorageAdapter implements StorageAdapter { this._mongoOptions = mongoOptions; this._mongoOptions.useNewUrlParser = true; this._mongoOptions.useUnifiedTopology = true; + this._onchange = () => {}; // MaxTimeMS is not a global MongoDB client option, it is applied per operation. this._maxTimeMS = mongoOptions.maxTimeMS; this.canSortOnJoinTables = true; + this.enableSchemaHooks = !!mongoOptions.enableSchemaHooks; + delete mongoOptions.enableSchemaHooks; delete mongoOptions.maxTimeMS; } + watch(callback: () => void): void { + this._onchange = callback; + } + connect() { if (this.connectionPromise) { return this.connectionPromise; @@ -198,7 +208,13 @@ export class MongoStorageAdapter implements StorageAdapter { _schemaCollection(): Promise { return this.connect() .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) { diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index e3ea204be4..b653ab4806 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -4,6 +4,8 @@ import { createClient } from './PostgresClient'; import Parse from 'parse/node'; // @flow-disable-next import _ from 'lodash'; +// @flow-disable-next +import { v4 as uuidv4 } from 'uuid'; import sql from './sql'; const PostgresRelationDoesNotExistError = '42P01'; @@ -794,20 +796,33 @@ const buildWhereClause = ({ schema, query, index, caseInsensitive }): WhereClaus export class PostgresStorageAdapter implements StorageAdapter { canSortOnJoinTables: boolean; + enableSchemaHooks: boolean; // Private _collectionPrefix: string; _client: any; + _onchange: any; _pgp: any; + _stream: any; + _uuid: any; - constructor({ uri, collectionPrefix = '', databaseOptions }: any) { + constructor({ uri, collectionPrefix = '', databaseOptions = {} }: any) { this._collectionPrefix = collectionPrefix; + this.enableSchemaHooks = !!databaseOptions.enableSchemaHooks; + delete databaseOptions.enableSchemaHooks; + const { client, pgp } = createClient(uri, databaseOptions); this._client = client; + this._onchange = () => {}; this._pgp = pgp; + this._uuid = uuidv4(); this.canSortOnJoinTables = false; } + watch(callback: () => void): void { + this._onchange = callback; + } + //Note that analyze=true will run the query, executing INSERTS, DELETES, etc. createExplainableQuery(query: string, analyze: boolean = false) { if (analyze) { @@ -818,12 +833,39 @@ export class PostgresStorageAdapter implements StorageAdapter { } handleShutdown() { + if (this._stream) { + this._stream.done(); + delete this._stream; + } if (!this._client) { return; } 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) { conn = conn || this._client; await conn @@ -859,6 +901,7 @@ export class PostgresStorageAdapter implements StorageAdapter { values ); }); + this._notifySchemaChange(); } async setIndexesWithSchemaFormat( @@ -920,11 +963,12 @@ export class PostgresStorageAdapter implements StorageAdapter { [className, 'schema', 'indexes', JSON.stringify(existingIndexes)] ); }); + this._notifySchemaChange(); } async createClass(className: string, schema: SchemaType, conn: ?any) { conn = conn || this._client; - return conn + const parseSchema = await conn .tx('create-class', async t => { await this.createTable(className, schema, t); await t.none( @@ -940,6 +984,8 @@ export class PostgresStorageAdapter implements StorageAdapter { } throw err; }); + this._notifySchemaChange(); + return parseSchema; } // 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.) @@ -1085,9 +1132,12 @@ export class PostgresStorageAdapter implements StorageAdapter { values: [className], }, ]; - return this._client + const response = await this._client .tx(t => t.none(this._pgp.helpers.concat(operations))) .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. @@ -1173,6 +1223,7 @@ export class PostgresStorageAdapter implements StorageAdapter { 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 @@ -2237,6 +2288,7 @@ export class PostgresStorageAdapter implements StorageAdapter { }) .then(() => this.schemaUpgrade(schema.className, schema)); }); + promises.push(this._listenToSchema()); return Promise.all(promises) .then(() => { return this._client.tx('perform-initialization', async t => { diff --git a/src/Adapters/Storage/StorageAdapter.js b/src/Adapters/Storage/StorageAdapter.js index 7e210d15a2..d46265f64f 100644 --- a/src/Adapters/Storage/StorageAdapter.js +++ b/src/Adapters/Storage/StorageAdapter.js @@ -111,6 +111,7 @@ export interface StorageAdapter { explain?: boolean ): Promise; performInitialization(options: ?any): Promise; + watch(callback: () => void): void; // Indexing createIndexes(className: string, indexes: any, conn: ?any): Promise; diff --git a/src/Config.js b/src/Config.js index 521f354d8e..302347c5ed 100644 --- a/src/Config.js +++ b/src/Config.js @@ -3,7 +3,6 @@ // mount is the URL for the root of the API; includes http, domain, etc. import AppCache from './cache'; -import SchemaCache from './Controllers/SchemaCache'; import DatabaseController from './Controllers/DatabaseController'; import net from 'net'; import { @@ -35,12 +34,7 @@ export class Config { config.applicationId = applicationId; Object.keys(cacheInfo).forEach(key => { if (key == 'databaseController') { - const schemaCache = new SchemaCache( - cacheInfo.cacheController, - cacheInfo.schemaCacheTTL, - cacheInfo.enableSingleSchemaCache - ); - config.database = new DatabaseController(cacheInfo.databaseController.adapter, schemaCache); + config.database = new DatabaseController(cacheInfo.databaseController.adapter); } else { config[key] = cacheInfo[key]; } diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index baf151686e..be2e61ab42 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -13,6 +13,9 @@ import deepcopy from 'deepcopy'; import logger from '../logger'; import * as SchemaController from './SchemaController'; 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'; function addWriteACL(query, acl) { @@ -230,9 +233,6 @@ const filterSensitiveData = ( return object; }; -import type { LoadSchemaOptions } from './types'; -import MongoStorageAdapter from '../Adapters/Storage/Mongo/MongoStorageAdapter'; - // Runs an update on the database. // Returns a promise for an object with the new values for field // modifications that don't know their results ahead of time, like @@ -398,9 +398,8 @@ class DatabaseController { schemaPromise: ?Promise; _transactionalSession: ?any; - constructor(adapter: StorageAdapter, schemaCache: any) { + constructor(adapter: StorageAdapter) { this.adapter = adapter; - this.schemaCache = schemaCache; // We don't want a mutable this.schema, because then you could have // one request that uses different schemas for different parts of // it. Instead, use loadSchema to get a schema. @@ -434,7 +433,7 @@ class DatabaseController { if (this.schemaPromise != null) { return this.schemaPromise; } - this.schemaPromise = SchemaController.load(this.adapter, this.schemaCache, options); + this.schemaPromise = SchemaController.load(this.adapter, options); this.schemaPromise.then( () => delete this.schemaPromise, () => delete this.schemaPromise @@ -916,7 +915,8 @@ class DatabaseController { */ deleteEverything(fast: boolean = false): Promise { 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. @@ -1325,8 +1325,12 @@ class DatabaseController { } deleteSchema(className: string): Promise { + let schemaController; return this.loadSchema({ clearCache: true }) - .then(schemaController => schemaController.getOneSchema(className, true)) + .then(s => { + schemaController = s; + return schemaController.getOneSchema(className, true); + }) .catch(error => { if (error === undefined) { return { fields: {} }; @@ -1356,7 +1360,8 @@ class DatabaseController { this.adapter.deleteClass(joinTableName(className, name)) ) ).then(() => { - return; + SchemaCache.del(className); + return schemaController.reloadData(); }); } else { return Promise.resolve(); @@ -1688,108 +1693,64 @@ class DatabaseController { ...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')); - const roleClassPromise = this.loadSchema().then(schema => schema.enforceClassExists('_Role')); - const idempotencyClassPromise = - this.adapter instanceof MongoStorageAdapter - ? this.loadSchema().then(schema => schema.enforceClassExists('_Idempotency')) - : Promise.resolve(); + await this.adapter.ensureUniqueness('_User', requiredUserFields, ['username']).catch(error => { + logger.warn('Unable to ensure uniqueness for usernames: ', error); + throw error; + }); - const usernameUniqueness = userClassPromise - .then(() => this.adapter.ensureUniqueness('_User', requiredUserFields, ['username'])) + await this.adapter + .ensureIndex('_User', requiredUserFields, ['username'], 'case_insensitive_username', true) .catch(error => { - logger.warn('Unable to ensure uniqueness for usernames: ', error); + logger.warn('Unable to create case insensitive username index: ', error); throw error; }); - - const usernameCaseInsensitiveIndex = userClassPromise - .then(() => - this.adapter.ensureIndex( - '_User', - requiredUserFields, - ['username'], - 'case_insensitive_username', - true - ) - ) + await this.adapter + .ensureIndex('_User', requiredUserFields, ['username'], 'case_insensitive_username', true) .catch(error => { logger.warn('Unable to create case insensitive username index: ', error); throw error; }); - const emailUniqueness = userClassPromise - .then(() => this.adapter.ensureUniqueness('_User', requiredUserFields, ['email'])) - .catch(error => { - logger.warn('Unable to ensure uniqueness for user email addresses: ', error); - throw error; - }); + await this.adapter.ensureUniqueness('_User', requiredUserFields, ['email']).catch(error => { + logger.warn('Unable to ensure uniqueness for user email addresses: ', error); + throw error; + }); - const emailCaseInsensitiveIndex = userClassPromise - .then(() => - this.adapter.ensureIndex( - '_User', - requiredUserFields, - ['email'], - 'case_insensitive_email', - true - ) - ) + await this.adapter + .ensureIndex('_User', requiredUserFields, ['email'], 'case_insensitive_email', true) .catch(error => { logger.warn('Unable to create case insensitive email index: ', error); throw error; }); - const roleUniqueness = roleClassPromise - .then(() => this.adapter.ensureUniqueness('_Role', requiredRoleFields, ['name'])) - .catch(error => { - logger.warn('Unable to ensure uniqueness for role name: ', error); - throw error; - }); + await this.adapter.ensureUniqueness('_Role', requiredRoleFields, ['name']).catch(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 = - this.adapter instanceof MongoStorageAdapter - ? idempotencyClassPromise - .then(() => - this.adapter.ensureUniqueness('_Idempotency', requiredIdempotencyFields, ['reqId']) - ) - .catch(error => { - logger.warn('Unable to ensure uniqueness for idempotency request ID: ', error); - throw error; - }) - : 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, - ]); + await 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; + }); + } + await this.adapter.updateSchemaWithIndexes(); } static _validateQuery: any => void; diff --git a/src/Controllers/SchemaCache.js b/src/Controllers/SchemaCache.js deleted file mode 100644 index 48f1f77ff0..0000000000 --- a/src/Controllers/SchemaCache.js +++ /dev/null @@ -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); - } -} diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index 8253746f8a..90f32b0b16 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -17,6 +17,7 @@ // @flow-disable-next const Parse = require('parse/node').Parse; import { StorageAdapter } from '../Adapters/Storage/StorageAdapter'; +import SchemaCache from '../Adapters/Cache/SchemaCache'; import DatabaseController from './DatabaseController'; import Config from '../Config'; // @flow-disable-next @@ -682,15 +683,13 @@ const typeToString = (type: SchemaField | string): string => { export default class SchemaController { _dbAdapter: StorageAdapter; schemaData: { [string]: Schema }; - _cache: any; reloadDataPromise: ?Promise; protectedFields: any; userIdRegEx: RegExp; - constructor(databaseAdapter: StorageAdapter, schemaCache: any) { + constructor(databaseAdapter: StorageAdapter) { this._dbAdapter = databaseAdapter; - this._cache = schemaCache; - this.schemaData = new SchemaData(); + this.schemaData = new SchemaData(SchemaCache.all(), this.protectedFields); this.protectedFields = Config.get(Parse.applicationId).protectedFields; const customIds = Config.get(Parse.applicationId).allowCustomObjectId; @@ -699,6 +698,10 @@ export default class SchemaController { const autoIdRegEx = /^[a-zA-Z0-9]{1,}$/; this.userIdRegEx = customIds ? customIdRegEx : autoIdRegEx; + + this._dbAdapter.watch(() => { + this.reloadData({ clearCache: true }); + }); } reloadData(options: LoadSchemaOptions = { clearCache: false }): Promise { @@ -725,12 +728,11 @@ export default class SchemaController { if (options.clearCache) { return this.setAllClasses(); } - return this._cache.getAllClasses().then(allClasses => { - if (allClasses && allClasses.length) { - return Promise.resolve(allClasses); - } - return this.setAllClasses(); - }); + const cached = SchemaCache.all(); + if (cached && cached.length) { + return Promise.resolve(cached); + } + return this.setAllClasses(); } setAllClasses(): Promise> { @@ -738,11 +740,7 @@ export default class SchemaController { .getAllClasses() .then(allSchemas => allSchemas.map(injectDefaultSchema)) .then(allSchemas => { - /* eslint-disable no-console */ - this._cache - .setAllClasses(allSchemas) - .catch(error => console.error('Error saving schema to cache:', error)); - /* eslint-enable no-console */ + SchemaCache.put(allSchemas); return allSchemas; }); } @@ -752,32 +750,28 @@ export default class SchemaController { allowVolatileClasses: boolean = false, options: LoadSchemaOptions = { clearCache: false } ): Promise { - let promise = Promise.resolve(); if (options.clearCache) { - promise = this._cache.clear(); + SchemaCache.clear(); } - return promise.then(() => { - if (allowVolatileClasses && volatileClasses.indexOf(className) > -1) { - const data = this.schemaData[className]; - return Promise.resolve({ - className, - fields: data.fields, - classLevelPermissions: data.classLevelPermissions, - 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; - }); + if (allowVolatileClasses && volatileClasses.indexOf(className) > -1) { + const data = this.schemaData[className]; + return Promise.resolve({ + className, + fields: data.fields, + classLevelPermissions: data.classLevelPermissions, + indexes: data.indexes, }); + } + 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 // have authorization (master key, or client class creation // enabled) before calling this function. - addClassIfNotExists( + async addClassIfNotExists( className: string, fields: SchemaFields = {}, classLevelPermissions: any, @@ -803,9 +797,8 @@ export default class SchemaController { } return Promise.reject(validationError); } - - return this._dbAdapter - .createClass( + try { + const adapterSchema = await this._dbAdapter.createClass( className, convertSchemaToAdapterSchema({ fields, @@ -813,18 +806,18 @@ export default class SchemaController { indexes, className, }) - ) - .then(convertAdapterSchemaToParseSchema) - .catch(error => { - if (error && error.code === Parse.Error.DUPLICATE_VALUE) { - throw new Parse.Error( - Parse.Error.INVALID_CLASS_NAME, - `Class ${className} already exists.` - ); - } else { - throw error; - } - }); + ); + // TODO: Remove by updating schema cache directly + await this.reloadData({ clearCache: true }); + const parseSchema = convertAdapterSchemaToParseSchema(adapterSchema); + return parseSchema; + } catch (error) { + if (error && error.code === Parse.Error.DUPLICATE_VALUE) { + throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`); + } else { + throw error; + } + } } updateClass( @@ -938,9 +931,8 @@ export default class SchemaController { } // We don't have this class. Update the schema return ( + // The schema update succeeded. Reload the schema this.addClassIfNotExists(className) - // The schema update succeeded. Reload the schema - .then(() => this.reloadData({ clearCache: true })) .catch(() => { // The schema update failed. This can be okay - it might // 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. - setPermissions(className: string, perms: any, newSchema: SchemaFields) { + async setPermissions(className: string, perms: any, newSchema: SchemaFields) { if (typeof perms === 'undefined') { return Promise.resolve(); } 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 @@ -1203,7 +1199,9 @@ export default class SchemaController { ); }); }) - .then(() => this._cache.clear()); + .then(() => { + SchemaCache.clear(); + }); } // Validates an object provided in REST format. @@ -1245,6 +1243,7 @@ export default class SchemaController { const enforceFields = results.filter(result => !!result); if (enforceFields.length !== 0) { + // TODO: Remove by updating schema cache directly await this.reloadData({ clearCache: true }); } this.ensureFields(enforceFields); @@ -1413,12 +1412,8 @@ export default class SchemaController { } // Returns a promise for a new Schema. -const load = ( - dbAdapter: StorageAdapter, - schemaCache: any, - options: any -): Promise => { - const schema = new SchemaController(dbAdapter, schemaCache); +const load = (dbAdapter: StorageAdapter, options: any): Promise => { + const schema = new SchemaController(dbAdapter); return schema.reloadData(options).then(() => schema); }; diff --git a/src/Controllers/index.js b/src/Controllers/index.js index 1e4765b666..89dc79c232 100644 --- a/src/Controllers/index.js +++ b/src/Controllers/index.js @@ -15,7 +15,6 @@ import { PushController } from './PushController'; import { PushQueue } from '../Push/PushQueue'; import { PushWorker } from '../Push/PushWorker'; import DatabaseController from './DatabaseController'; -import SchemaCache from './SchemaCache'; // Adapters 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 ParsePushAdapter from '@parse/push-adapter'; import ParseGraphQLController from './ParseGraphQLController'; +import SchemaCache from '../Adapters/Cache/SchemaCache'; export function getControllers(options: ParseServerOptions) { const loggerController = getLoggerController(options); @@ -41,7 +41,7 @@ export function getControllers(options: ParseServerOptions) { const cacheController = getCacheController(options); const analyticsController = getAnalyticsController(options); const liveQueryController = getLiveQueryController(options); - const databaseController = getDatabaseController(options, cacheController); + const databaseController = getDatabaseController(options); const hooksController = getHooksController(options, databaseController); const authDataManager = getAuthDataManager(options); const parseGraphQLController = getParseGraphQLController(options, { @@ -64,6 +64,7 @@ export function getControllers(options: ParseServerOptions) { databaseController, hooksController, authDataManager, + schemaCache: SchemaCache, }; } @@ -141,17 +142,8 @@ export function getLiveQueryController(options: ParseServerOptions): LiveQueryCo return new LiveQueryController(options.liveQuery); } -export function getDatabaseController( - options: ParseServerOptions, - cacheController: CacheController -): DatabaseController { - const { - databaseURI, - databaseOptions, - collectionPrefix, - schemaCacheTTL, - enableSingleSchemaCache, - } = options; +export function getDatabaseController(options: ParseServerOptions): DatabaseController { + const { databaseURI, collectionPrefix, databaseOptions } = options; let { databaseAdapter } = options; if ( (databaseOptions || @@ -165,10 +157,7 @@ export function getDatabaseController( } else { databaseAdapter = loadAdapter(databaseAdapter); } - return new DatabaseController( - databaseAdapter, - new SchemaCache(cacheController, schemaCacheTTL, enableSingleSchemaCache) - ); + return new DatabaseController(databaseAdapter); } export function getHooksController( diff --git a/src/GraphQL/ParseGraphQLSchema.js b/src/GraphQL/ParseGraphQLSchema.js index 096266442d..d194a40ce5 100644 --- a/src/GraphQL/ParseGraphQLSchema.js +++ b/src/GraphQL/ParseGraphQLSchema.js @@ -11,6 +11,7 @@ import * as defaultGraphQLQueries from './loaders/defaultGraphQLQueries'; import * as defaultGraphQLMutations from './loaders/defaultGraphQLMutations'; import ParseGraphQLController, { ParseGraphQLConfig } from '../Controllers/ParseGraphQLController'; import DatabaseController from '../Controllers/DatabaseController'; +import SchemaCache from '../Adapters/Cache/SchemaCache'; import { toGraphQLError } from './parseGraphQLUtils'; import * as schemaDirectives from './loaders/schemaDirectives'; import * as schemaTypes from './loaders/schemaTypes'; @@ -66,6 +67,7 @@ class ParseGraphQLSchema { log: any; appId: string; graphQLCustomTypeDefs: ?(string | GraphQLSchema | DocumentNode | GraphQLNamedType[]); + schemaCache: any; constructor( params: { @@ -85,6 +87,7 @@ class ParseGraphQLSchema { this.log = params.log || requiredParameter('You must provide a log instance!'); this.graphQLCustomTypeDefs = params.graphQLCustomTypeDefs; this.appId = params.appId || requiredParameter('You must provide the appId!'); + this.schemaCache = SchemaCache; } async load() { diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index de92038964..df4718c4ae 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -100,7 +100,7 @@ module.exports.ParseServerOptions = { }, databaseOptions: { env: 'PARSE_SERVER_DATABASE_OPTIONS', - help: 'Options to pass to the mongodb client', + help: 'Options to pass to the database client', action: parsers.objectParser, }, databaseURI: { @@ -149,13 +149,6 @@ module.exports.ParseServerOptions = { action: parsers.booleanParser, 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: { env: 'PARSE_SERVER_ENCRYPTION_KEY', help: 'Key for encrypting your files', @@ -366,13 +359,6 @@ module.exports.ParseServerOptions = { action: parsers.booleanParser, 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: { env: 'PARSE_SERVER_SECURITY', help: 'The security options to identify and report weak security settings.', @@ -788,3 +774,12 @@ module.exports.FileUploadOptions = { 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, + }, +}; diff --git a/src/Options/docs.js b/src/Options/docs.js index 86c27b761d..b8ae2beccf 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -18,7 +18,7 @@ * @property {String} collectionPrefix A collection prefix for the classes * @property {CustomPagesOptions} customPages custom pages for password validation and reset * @property {Adapter} 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 {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 @@ -27,7 +27,6 @@ * @property {Number} emailVerifyTokenValidityDuration Email verification token validity duration, in seconds * @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} 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 {Boolean} expireInactiveSessions Sets wether we should expire the inactive sessions, defaults to true * @property {String} fileKey Key for your files @@ -67,7 +66,6 @@ * @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} 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 {Function} serverCloseComplete Callback when server has closed * @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} 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. + */ diff --git a/src/Options/index.js b/src/Options/index.js index 6b4a504801..e413d2b9c1 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -63,8 +63,9 @@ export interface ParseServerOptions { /* The full URI to your database. Supported databases are mongodb or postgres. :DEFAULT: mongodb://localhost:27017/parse */ databaseURI: string; - /* Options to pass to the mongodb client */ - databaseOptions: ?any; + /* Options to pass to the database client + :ENV: PARSE_SERVER_DATABASE_OPTIONS */ + databaseOptions: ?DatabaseOptions; /* Adapter module for the database */ databaseAdapter: ?Adapter; /* 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. :DEFAULT: true */ 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) :DEFAULT: 5000 */ cacheTTL: ?number; @@ -171,9 +169,6 @@ export interface ParseServerOptions { :ENV: PARSE_SERVER_ENABLE_EXPERIMENTAL_DIRECT_ACCESS :DEFAULT: false */ 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 :DEFAULT: false */ enableExpressErrorHandler: ?boolean; @@ -416,3 +411,9 @@ export interface FileUploadOptions { :DEFAULT: false */ 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; +} diff --git a/src/PromiseRouter.js b/src/PromiseRouter.js index 1f531025a9..45f600f31b 100644 --- a/src/PromiseRouter.js +++ b/src/PromiseRouter.js @@ -150,7 +150,6 @@ function makeExpressHandler(appId, promiseHandler) { promiseHandler(req) .then( result => { - clearSchemaCache(req); if (!result.response && !result.location && !result.text) { log.error('the handler did not include a "response" or a "location" field'); throw 'control should not get here'; @@ -184,17 +183,14 @@ function makeExpressHandler(appId, promiseHandler) { res.json(result.response); }, error => { - clearSchemaCache(req); next(error); } ) .catch(e => { - clearSchemaCache(req); log.error(`Error generating response. ${inspect(e)}`, { error: e }); next(e); }); } catch (e) { - clearSchemaCache(req); log.error(`Error handling request: ${inspect(e)}`, { error: e }); next(e); } @@ -212,9 +208,3 @@ function maskSensitiveUrl(req) { } return maskUrl; } - -function clearSchemaCache(req) { - if (req.config && !req.config.enableSingleSchemaCache) { - req.config.database.schemaCache.clear(); - } -}