From cc6d474dcb49de39b55e681d319533686b544516 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Thu, 30 May 2019 11:14:05 -0500 Subject: [PATCH] Schema Cache Improvement 2 (#5616) * schema hasClass improvement * create object improvement * destroy object * update object * hasClass test rewrite * more tests * improve signing up users --- spec/RedisCacheAdapter.spec.js | 142 +++++- spec/RestQuery.spec.js | 39 +- spec/Schema.spec.js | 1 + spec/rest.spec.js | 37 +- src/Controllers/DatabaseController.js | 674 +++++++++++++------------- src/Controllers/SchemaController.js | 10 +- src/RestWrite.js | 48 +- src/rest.js | 26 +- 8 files changed, 572 insertions(+), 405 deletions(-) diff --git a/spec/RedisCacheAdapter.spec.js b/spec/RedisCacheAdapter.spec.js index 637d9d67..7dc826d1 100644 --- a/spec/RedisCacheAdapter.spec.js +++ b/spec/RedisCacheAdapter.spec.js @@ -175,20 +175,20 @@ describe_only(() => { beforeEach(async () => { await cacheAdapter.clear(); - getSpy = spyOn(cacheAdapter, 'get').and.callThrough(); - putSpy = spyOn(cacheAdapter, 'put').and.callThrough(); await reconfigureServer({ cacheAdapter, enableSingleSchemaCache: true, }); + getSpy = spyOn(cacheAdapter, 'get').and.callThrough(); + putSpy = spyOn(cacheAdapter, 'put').and.callThrough(); }); it('test new object', async () => { const object = new TestObject(); object.set('foo', 'bar'); await object.save(); - expect(getSpy.calls.count()).toBe(4); - expect(putSpy.calls.count()).toBe(3); + expect(getSpy.calls.count()).toBe(2); + expect(putSpy.calls.count()).toBe(2); }); it('test new object multiple fields', async () => { @@ -200,8 +200,8 @@ describe_only(() => { booleanField: true, }); await container.save(); - expect(getSpy.calls.count()).toBe(4); - expect(putSpy.calls.count()).toBe(3); + expect(getSpy.calls.count()).toBe(2); + expect(putSpy.calls.count()).toBe(2); }); it('test update existing fields', async () => { @@ -214,7 +214,57 @@ describe_only(() => { object.set('foo', 'barz'); await object.save(); - expect(getSpy.calls.count()).toBe(3); + expect(getSpy.calls.count()).toBe(2); + expect(putSpy.calls.count()).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(11); + expect(putSpy.calls.count()).toBe(10); + + getSpy.calls.reset(); + putSpy.calls.reset(); + + await Parse.Object.destroyAll(objects); + expect(getSpy.calls.count()).toBe(11); + expect(putSpy.calls.count()).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(12); + expect(putSpy.calls.count()).toBe(5); + + getSpy.calls.reset(); + putSpy.calls.reset(); + + await Parse.Object.destroyAll(objects, { batchSize: 5 }); + expect(getSpy.calls.count()).toBe(12); expect(putSpy.calls.count()).toBe(0); }); @@ -228,7 +278,7 @@ describe_only(() => { object.set('new', 'barz'); await object.save(); - expect(getSpy.calls.count()).toBe(3); + expect(getSpy.calls.count()).toBe(2); expect(putSpy.calls.count()).toBe(1); }); @@ -248,8 +298,43 @@ describe_only(() => { booleanField: true, }); await object.save(); + expect(getSpy.calls.count()).toBe(2); + expect(putSpy.calls.count()).toBe(1); + }); + + it('test user', async () => { + const user = new Parse.User(); + user.setUsername('testing'); + user.setPassword('testing'); + await user.signUp(); + + expect(getSpy.calls.count()).toBe(6); + expect(putSpy.calls.count()).toBe(1); + }); + + it('test allowClientCreation false', async () => { + const object = new TestObject(); + await object.save(); + await reconfigureServer({ + cacheAdapter, + enableSingleSchemaCache: true, + allowClientClassCreation: false, + }); + getSpy.calls.reset(); + putSpy.calls.reset(); + + object.set('foo', 'bar'); + await object.save(); expect(getSpy.calls.count()).toBe(3); expect(putSpy.calls.count()).toBe(1); + + 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(0); }); it('test query', async () => { @@ -266,6 +351,45 @@ describe_only(() => { expect(putSpy.calls.count()).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(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(0); + }); + it('test delete object', async () => { const object = new TestObject(); object.set('foo', 'bar'); @@ -275,7 +399,7 @@ describe_only(() => { putSpy.calls.reset(); await object.destroy(); - expect(getSpy.calls.count()).toBe(3); + expect(getSpy.calls.count()).toBe(2); expect(putSpy.calls.count()).toBe(0); }); diff --git a/spec/RestQuery.spec.js b/spec/RestQuery.spec.js index 51a49b08..66af50c8 100644 --- a/spec/RestQuery.spec.js +++ b/spec/RestQuery.spec.js @@ -181,31 +181,26 @@ describe('rest query', () => { ); }); - it('query existent class when disabled client class creation', done => { + it('query existent class when disabled client class creation', async () => { const customConfig = Object.assign({}, config, { allowClientClassCreation: false, }); - config.database - .loadSchema() - .then(schema => schema.addClassIfNotExists('ClientClassCreation', {})) - .then(actualSchema => { - expect(actualSchema.className).toEqual('ClientClassCreation'); - return rest.find( - customConfig, - auth.nobody(customConfig), - 'ClientClassCreation', - {} - ); - }) - .then( - result => { - expect(result.results.length).toEqual(0); - done(); - }, - () => { - fail('Should not throw error'); - } - ); + const schema = await config.database.loadSchema(); + const actualSchema = await schema.addClassIfNotExists( + 'ClientClassCreation', + {} + ); + expect(actualSchema.className).toEqual('ClientClassCreation'); + + await schema.reloadData({ clearCache: true }); + // Should not throw + const result = await rest.find( + customConfig, + auth.nobody(customConfig), + 'ClientClassCreation', + {} + ); + expect(result.results.length).toEqual(0); }); it('query with wrongly encoded parameter', done => { diff --git a/spec/Schema.spec.js b/spec/Schema.spec.js index 716778ce..54221fd5 100644 --- a/spec/Schema.spec.js +++ b/spec/Schema.spec.js @@ -929,6 +929,7 @@ describe('SchemaController', () => { .then(schema => { return schema .addClassIfNotExists('NewClass', {}) + .then(() => schema.reloadData({ clearCache: true })) .then(() => { schema .hasClass('NewClass') diff --git a/spec/rest.spec.js b/spec/rest.spec.js index 045ad362..ca0294d4 100644 --- a/spec/rest.spec.js +++ b/spec/rest.spec.js @@ -181,30 +181,25 @@ describe('rest create', () => { ); }); - it('handles create on existent class when disabled client class creation', done => { + it('handles create on existent class when disabled client class creation', async () => { const customConfig = Object.assign({}, config, { allowClientClassCreation: false, }); - config.database - .loadSchema() - .then(schema => schema.addClassIfNotExists('ClientClassCreation', {})) - .then(actualSchema => { - expect(actualSchema.className).toEqual('ClientClassCreation'); - return rest.create( - customConfig, - auth.nobody(customConfig), - 'ClientClassCreation', - {} - ); - }) - .then( - () => { - done(); - }, - () => { - fail('Should not throw error'); - } - ); + const schema = await config.database.loadSchema(); + const actualSchema = await schema.addClassIfNotExists( + 'ClientClassCreation', + {} + ); + expect(actualSchema.className).toEqual('ClientClassCreation'); + + await schema.reloadData({ clearCache: true }); + // Should not throw + await rest.create( + customConfig, + auth.nobody(customConfig), + 'ClientClassCreation', + {} + ); }); it('handles user signup', done => { diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 76efd6a0..473fcf02 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -432,6 +432,15 @@ class DatabaseController { return this.loadSchema(options); } + loadSchemaIfNeeded( + schemaController: SchemaController.SchemaController, + options: LoadSchemaOptions = { clearCache: false } + ): Promise { + return schemaController + ? Promise.resolve(schemaController) + : this.loadSchema(options); + } + // Returns a promise for the classname that is related to the given // classname through the key. // TODO: make this not in the DatabaseController interface @@ -477,7 +486,8 @@ class DatabaseController { update: any, { acl, many, upsert }: FullQueryOptions = {}, skipSanitization: boolean = false, - validateOnly: boolean = false + validateOnly: boolean = false, + validSchemaController: SchemaController.SchemaController ): Promise { const originalQuery = query; const originalUpdate = update; @@ -486,141 +496,145 @@ class DatabaseController { var relationUpdates = []; var isMaster = acl === undefined; var aclGroup = acl || []; - return this.loadSchema().then(schemaController => { - return (isMaster - ? Promise.resolve() - : schemaController.validatePermission(className, aclGroup, 'update') - ) - .then(() => { - relationUpdates = this.collectRelationUpdates( - className, - originalQuery.objectId, - update - ); - if (!isMaster) { - query = this.addPointerPermissions( - schemaController, + + return this.loadSchemaIfNeeded(validSchemaController).then( + schemaController => { + return (isMaster + ? Promise.resolve() + : schemaController.validatePermission(className, aclGroup, 'update') + ) + .then(() => { + relationUpdates = this.collectRelationUpdates( className, - 'update', - query, - aclGroup + originalQuery.objectId, + update ); - } - if (!query) { - return Promise.resolve(); - } - if (acl) { - query = addWriteACL(query, acl); - } - validateQuery(query); - return schemaController - .getOneSchema(className, true) - .catch(error => { - // If the schema doesn't exist, pretend it exists with no fields. This behavior - // will likely need revisiting. - if (error === undefined) { - return { fields: {} }; - } - throw error; - }) - .then(schema => { - Object.keys(update).forEach(fieldName => { - if (fieldName.match(/^authData\.([a-zA-Z0-9_]+)\.id$/)) { - throw new Parse.Error( - Parse.Error.INVALID_KEY_NAME, - `Invalid field name for update: ${fieldName}` - ); + if (!isMaster) { + query = this.addPointerPermissions( + schemaController, + className, + 'update', + query, + aclGroup + ); + } + if (!query) { + return Promise.resolve(); + } + if (acl) { + query = addWriteACL(query, acl); + } + validateQuery(query); + return schemaController + .getOneSchema(className, true) + .catch(error => { + // If the schema doesn't exist, pretend it exists with no fields. This behavior + // will likely need revisiting. + if (error === undefined) { + return { fields: {} }; } - const rootFieldName = getRootFieldName(fieldName); - if ( - !SchemaController.fieldNameIsValid(rootFieldName) && - !isSpecialUpdateKey(rootFieldName) - ) { - throw new Parse.Error( - Parse.Error.INVALID_KEY_NAME, - `Invalid field name for update: ${fieldName}` + throw error; + }) + .then(schema => { + Object.keys(update).forEach(fieldName => { + if (fieldName.match(/^authData\.([a-zA-Z0-9_]+)\.id$/)) { + throw new Parse.Error( + Parse.Error.INVALID_KEY_NAME, + `Invalid field name for update: ${fieldName}` + ); + } + const rootFieldName = getRootFieldName(fieldName); + if ( + !SchemaController.fieldNameIsValid(rootFieldName) && + !isSpecialUpdateKey(rootFieldName) + ) { + throw new Parse.Error( + Parse.Error.INVALID_KEY_NAME, + `Invalid field name for update: ${fieldName}` + ); + } + }); + for (const updateOperation in update) { + if ( + update[updateOperation] && + typeof update[updateOperation] === 'object' && + Object.keys(update[updateOperation]).some( + innerKey => + innerKey.includes('$') || innerKey.includes('.') + ) + ) { + throw new Parse.Error( + Parse.Error.INVALID_NESTED_KEY, + "Nested keys should not contain the '$' or '.' characters" + ); + } + } + update = transformObjectACL(update); + transformAuthData(className, update, schema); + if (validateOnly) { + return this.adapter + .find(className, schema, query, {}) + .then(result => { + if (!result || !result.length) { + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Object not found.' + ); + } + return {}; + }); + } + if (many) { + return this.adapter.updateObjectsByQuery( + className, + schema, + query, + update + ); + } else if (upsert) { + return this.adapter.upsertOneObject( + className, + schema, + query, + update + ); + } else { + return this.adapter.findOneAndUpdate( + className, + schema, + query, + update ); } }); - for (const updateOperation in update) { - if ( - update[updateOperation] && - typeof update[updateOperation] === 'object' && - Object.keys(update[updateOperation]).some( - innerKey => innerKey.includes('$') || innerKey.includes('.') - ) - ) { - throw new Parse.Error( - Parse.Error.INVALID_NESTED_KEY, - "Nested keys should not contain the '$' or '.' characters" - ); - } - } - update = transformObjectACL(update); - transformAuthData(className, update, schema); - if (validateOnly) { - return this.adapter - .find(className, schema, query, {}) - .then(result => { - if (!result || !result.length) { - throw new Parse.Error( - Parse.Error.OBJECT_NOT_FOUND, - 'Object not found.' - ); - } - return {}; - }); - } - if (many) { - return this.adapter.updateObjectsByQuery( - className, - schema, - query, - update - ); - } else if (upsert) { - return this.adapter.upsertOneObject( - className, - schema, - query, - update - ); - } else { - return this.adapter.findOneAndUpdate( - className, - schema, - query, - update - ); - } + }) + .then((result: any) => { + if (!result) { + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Object not found.' + ); + } + if (validateOnly) { + return result; + } + return this.handleRelationUpdates( + className, + originalQuery.objectId, + update, + relationUpdates + ).then(() => { + return result; }); - }) - .then((result: any) => { - if (!result) { - throw new Parse.Error( - Parse.Error.OBJECT_NOT_FOUND, - 'Object not found.' - ); - } - if (validateOnly) { - return result; - } - return this.handleRelationUpdates( - className, - originalQuery.objectId, - update, - relationUpdates - ).then(() => { - return result; + }) + .then(result => { + if (skipSanitization) { + return Promise.resolve(result); + } + return sanitizeDatabaseResult(originalUpdate, result); }); - }) - .then(result => { - if (skipSanitization) { - return Promise.resolve(result); - } - return sanitizeDatabaseResult(originalUpdate, result); - }); - }); + } + ); } // Collect all relation-updating operations from a REST-format update. @@ -753,65 +767,68 @@ class DatabaseController { destroy( className: string, query: any, - { acl }: QueryOptions = {} + { acl }: QueryOptions = {}, + validSchemaController: SchemaController.SchemaController ): Promise { const isMaster = acl === undefined; const aclGroup = acl || []; - return this.loadSchema().then(schemaController => { - return (isMaster - ? Promise.resolve() - : schemaController.validatePermission(className, aclGroup, 'delete') - ).then(() => { - if (!isMaster) { - query = this.addPointerPermissions( - schemaController, - className, - 'delete', - query, - aclGroup - ); - if (!query) { - throw new Parse.Error( - Parse.Error.OBJECT_NOT_FOUND, - 'Object not found.' - ); - } - } - // delete by query - if (acl) { - query = addWriteACL(query, acl); - } - validateQuery(query); - return schemaController - .getOneSchema(className) - .catch(error => { - // If the schema doesn't exist, pretend it exists with no fields. This behavior - // will likely need revisiting. - if (error === undefined) { - return { fields: {} }; - } - throw error; - }) - .then(parseFormatSchema => - this.adapter.deleteObjectsByQuery( + return this.loadSchemaIfNeeded(validSchemaController).then( + schemaController => { + return (isMaster + ? Promise.resolve() + : schemaController.validatePermission(className, aclGroup, 'delete') + ).then(() => { + if (!isMaster) { + query = this.addPointerPermissions( + schemaController, className, - parseFormatSchema, - query - ) - ) - .catch(error => { - // When deleting sessions while changing passwords, don't throw an error if they don't have any sessions. - if ( - className === '_Session' && - error.code === Parse.Error.OBJECT_NOT_FOUND - ) { - return Promise.resolve({}); + 'delete', + query, + aclGroup + ); + if (!query) { + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Object not found.' + ); } - throw error; - }); - }); - }); + } + // delete by query + if (acl) { + query = addWriteACL(query, acl); + } + validateQuery(query); + return schemaController + .getOneSchema(className) + .catch(error => { + // If the schema doesn't exist, pretend it exists with no fields. This behavior + // will likely need revisiting. + if (error === undefined) { + return { fields: {} }; + } + throw error; + }) + .then(parseFormatSchema => + this.adapter.deleteObjectsByQuery( + className, + parseFormatSchema, + query + ) + ) + .catch(error => { + // When deleting sessions while changing passwords, don't throw an error if they don't have any sessions. + if ( + className === '_Session' && + error.code === Parse.Error.OBJECT_NOT_FOUND + ) { + return Promise.resolve({}); + } + throw error; + }); + }); + } + ); } // Inserts an object into the database. @@ -820,7 +837,8 @@ class DatabaseController { className: string, object: any, { acl }: QueryOptions = {}, - validateOnly: boolean = false + validateOnly: boolean = false, + validSchemaController: SchemaController.SchemaController ): Promise { // Make a copy of the object, so we don't mutate the incoming data. const originalObject = object; @@ -836,8 +854,9 @@ class DatabaseController { null, object ); + return this.validateClassName(className) - .then(() => this.loadSchema()) + .then(() => this.loadSchemaIfNeeded(validSchemaController)) .then(schemaController => { return (isMaster ? Promise.resolve() @@ -1173,7 +1192,8 @@ class DatabaseController { pipeline, readPreference, }: any = {}, - auth: any = {} + auth: any = {}, + validSchemaController: SchemaController.SchemaController ): Promise { const isMaster = acl === undefined; const aclGroup = acl || []; @@ -1186,153 +1206,157 @@ class DatabaseController { op = count === true ? 'count' : op; let classExists = true; - return this.loadSchema().then(schemaController => { - //Allow volatile classes if querying with Master (for _PushStatus) - //TODO: Move volatile classes concept into mongo adapter, postgres adapter shouldn't care - //that api.parse.com breaks when _PushStatus exists in mongo. - return schemaController - .getOneSchema(className, isMaster) - .catch(error => { - // Behavior for non-existent classes is kinda weird on Parse.com. Probably doesn't matter too much. - // For now, pretend the class exists but has no objects, - if (error === undefined) { - classExists = false; - return { fields: {} }; - } - throw error; - }) - .then(schema => { - // Parse.com treats queries on _created_at and _updated_at as if they were queries on createdAt and updatedAt, - // so duplicate that behavior here. If both are specified, the correct behavior to match Parse.com is to - // use the one that appears first in the sort list. - if (sort._created_at) { - sort.createdAt = sort._created_at; - delete sort._created_at; - } - if (sort._updated_at) { - sort.updatedAt = sort._updated_at; - delete sort._updated_at; - } - const queryOptions = { skip, limit, sort, keys, readPreference }; - Object.keys(sort).forEach(fieldName => { - if (fieldName.match(/^authData\.([a-zA-Z0-9_]+)\.id$/)) { - throw new Parse.Error( - Parse.Error.INVALID_KEY_NAME, - `Cannot sort by ${fieldName}` - ); + return this.loadSchemaIfNeeded(validSchemaController).then( + schemaController => { + //Allow volatile classes if querying with Master (for _PushStatus) + //TODO: Move volatile classes concept into mongo adapter, postgres adapter shouldn't care + //that api.parse.com breaks when _PushStatus exists in mongo. + return schemaController + .getOneSchema(className, isMaster) + .catch(error => { + // Behavior for non-existent classes is kinda weird on Parse.com. Probably doesn't matter too much. + // For now, pretend the class exists but has no objects, + if (error === undefined) { + classExists = false; + return { fields: {} }; } - const rootFieldName = getRootFieldName(fieldName); - if (!SchemaController.fieldNameIsValid(rootFieldName)) { - throw new Parse.Error( - Parse.Error.INVALID_KEY_NAME, - `Invalid field name: ${fieldName}.` - ); + throw error; + }) + .then(schema => { + // Parse.com treats queries on _created_at and _updated_at as if they were queries on createdAt and updatedAt, + // so duplicate that behavior here. If both are specified, the correct behavior to match Parse.com is to + // use the one that appears first in the sort list. + if (sort._created_at) { + sort.createdAt = sort._created_at; + delete sort._created_at; } - }); - return (isMaster - ? Promise.resolve() - : schemaController.validatePermission(className, aclGroup, op) - ) - .then(() => this.reduceRelationKeys(className, query, queryOptions)) - .then(() => - this.reduceInRelation(className, query, schemaController) - ) - .then(() => { - let protectedFields; - if (!isMaster) { - query = this.addPointerPermissions( - schemaController, - className, - op, - query, - aclGroup - ); - // ProtectedFields is generated before executing the query so we - // can optimize the query using Mongo Projection at a later stage. - protectedFields = this.addProtectedFields( - schemaController, - className, - query, - aclGroup, - auth + if (sort._updated_at) { + sort.updatedAt = sort._updated_at; + delete sort._updated_at; + } + const queryOptions = { skip, limit, sort, keys, readPreference }; + Object.keys(sort).forEach(fieldName => { + if (fieldName.match(/^authData\.([a-zA-Z0-9_]+)\.id$/)) { + throw new Parse.Error( + Parse.Error.INVALID_KEY_NAME, + `Cannot sort by ${fieldName}` ); } - if (!query) { - if (op === 'get') { - throw new Parse.Error( - Parse.Error.OBJECT_NOT_FOUND, - 'Object not found.' - ); - } else { - return []; - } - } - if (!isMaster) { - if (op === 'update' || op === 'delete') { - query = addWriteACL(query, aclGroup); - } else { - query = addReadACL(query, aclGroup); - } - } - validateQuery(query); - if (count) { - if (!classExists) { - return 0; - } else { - return this.adapter.count( - className, - schema, - query, - readPreference - ); - } - } else if (distinct) { - if (!classExists) { - return []; - } else { - return this.adapter.distinct( - className, - schema, - query, - distinct - ); - } - } else if (pipeline) { - if (!classExists) { - return []; - } else { - return this.adapter.aggregate( - className, - schema, - pipeline, - readPreference - ); - } - } else { - return this.adapter - .find(className, schema, query, queryOptions) - .then(objects => - objects.map(object => { - object = untransformObjectACL(object); - return filterSensitiveData( - isMaster, - aclGroup, - className, - protectedFields, - object - ); - }) - ) - .catch(error => { - throw new Parse.Error( - Parse.Error.INTERNAL_SERVER_ERROR, - error - ); - }); + const rootFieldName = getRootFieldName(fieldName); + if (!SchemaController.fieldNameIsValid(rootFieldName)) { + throw new Parse.Error( + Parse.Error.INVALID_KEY_NAME, + `Invalid field name: ${fieldName}.` + ); } }); - }); - }); + return (isMaster + ? Promise.resolve() + : schemaController.validatePermission(className, aclGroup, op) + ) + .then(() => + this.reduceRelationKeys(className, query, queryOptions) + ) + .then(() => + this.reduceInRelation(className, query, schemaController) + ) + .then(() => { + let protectedFields; + if (!isMaster) { + query = this.addPointerPermissions( + schemaController, + className, + op, + query, + aclGroup + ); + // ProtectedFields is generated before executing the query so we + // can optimize the query using Mongo Projection at a later stage. + protectedFields = this.addProtectedFields( + schemaController, + className, + query, + aclGroup, + auth + ); + } + if (!query) { + if (op === 'get') { + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Object not found.' + ); + } else { + return []; + } + } + if (!isMaster) { + if (op === 'update' || op === 'delete') { + query = addWriteACL(query, aclGroup); + } else { + query = addReadACL(query, aclGroup); + } + } + validateQuery(query); + if (count) { + if (!classExists) { + return 0; + } else { + return this.adapter.count( + className, + schema, + query, + readPreference + ); + } + } else if (distinct) { + if (!classExists) { + return []; + } else { + return this.adapter.distinct( + className, + schema, + query, + distinct + ); + } + } else if (pipeline) { + if (!classExists) { + return []; + } else { + return this.adapter.aggregate( + className, + schema, + pipeline, + readPreference + ); + } + } else { + return this.adapter + .find(className, schema, query, queryOptions) + .then(objects => + objects.map(object => { + object = untransformObjectACL(object); + return filterSensitiveData( + isMaster, + aclGroup, + className, + protectedFields, + object + ); + }) + ) + .catch(error => { + throw new Parse.Error( + Parse.Error.INTERNAL_SERVER_ERROR, + error + ); + }); + } + }); + }); + } + ); } deleteSchema(className: string): Promise { diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index 01dc5f60..fffc6789 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -646,7 +646,7 @@ export default class SchemaController { fields: SchemaFields = {}, classLevelPermissions: any, indexes: any = {} - ): Promise { + ): Promise { var validationError = this.validateNewClass( className, fields, @@ -667,11 +667,6 @@ export default class SchemaController { }) ) .then(convertAdapterSchemaToParseSchema) - .then(res => { - return this._cache.clear().then(() => { - return Promise.resolve(res); - }); - }) .catch(error => { if (error && error.code === Parse.Error.DUPLICATE_VALUE) { throw new Parse.Error( @@ -1285,6 +1280,9 @@ export default class SchemaController { // Checks if a given class is in the schema. hasClass(className: string) { + if (this.schemaData[className]) { + return Promise.resolve(true); + } return this.reloadData().then(() => !!this.schemaData[className]); } } diff --git a/src/RestWrite.js b/src/RestWrite.js index 22df2a97..998d3576 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -69,6 +69,10 @@ function RestWrite( // The timestamp we'll use for this whole operation this.updatedAt = Parse._encode(new Date()).iso; + + // Shared SchemaController to be reused to reduce the number of loadSchema() calls per request + // Once set the schemaData should be immutable + this.validSchemaController = null; } // A convenient method to perform all the steps of processing the @@ -101,7 +105,8 @@ RestWrite.prototype.execute = function() { .then(() => { return this.validateSchema(); }) - .then(() => { + .then(schemaController => { + this.validSchemaController = schemaController; return this.setRequiredFieldsIfNeeded(); }) .then(() => { @@ -614,7 +619,9 @@ RestWrite.prototype._validateUserName = function() { .find( this.className, { username: this.data.username, objectId: { $ne: this.objectId() } }, - { limit: 1 } + { limit: 1 }, + {}, + this.validSchemaController ) .then(results => { if (results.length > 0) { @@ -645,7 +652,9 @@ RestWrite.prototype._validateEmail = function() { .find( this.className, { email: this.data.email, objectId: { $ne: this.objectId() } }, - { limit: 1 } + { limit: 1 }, + {}, + this.validSchemaController ) .then(results => { if (results.length > 0) { @@ -854,11 +863,16 @@ RestWrite.prototype.destroyDuplicatedSessions = function() { if (!user.objectId) { return; } - this.config.database.destroy('_Session', { - user, - installationId, - sessionToken: { $ne: sessionToken }, - }); + this.config.database.destroy( + '_Session', + { + user, + installationId, + sessionToken: { $ne: sessionToken }, + }, + {}, + this.validSchemaController + ); }; // Handles any followup logic @@ -1361,7 +1375,15 @@ RestWrite.prototype.runDatabaseOperation = function() { return defer.then(() => { // Run an update return this.config.database - .update(this.className, this.query, this.data, this.runOptions) + .update( + this.className, + this.query, + this.data, + this.runOptions, + false, + false, + this.validSchemaController + ) .then(response => { response.updatedAt = this.updatedAt; this._updateResponseWithData(response, this.data); @@ -1391,7 +1413,13 @@ RestWrite.prototype.runDatabaseOperation = function() { // Run a create return this.config.database - .create(this.className, this.data, this.runOptions) + .create( + this.className, + this.data, + this.runOptions, + false, + this.validSchemaController + ) .catch(error => { if ( this.className !== '_User' || diff --git a/src/rest.js b/src/rest.js index 21bef439..c928c5af 100644 --- a/src/rest.js +++ b/src/rest.js @@ -102,6 +102,7 @@ function del(config, auth, className, objectId) { enforceRoleSecurity('delete', className, auth); let inflatedObject; + let schemaController; return Promise.resolve() .then(() => { @@ -151,8 +152,10 @@ function del(config, auth, className, objectId) { return; } }) - .then(() => { - var options = {}; + .then(() => config.database.loadSchema()) + .then(s => { + schemaController = s; + const options = {}; if (!auth.isMaster) { options.acl = ['*']; if (auth.user) { @@ -166,20 +169,19 @@ function del(config, auth, className, objectId) { { objectId: objectId, }, - options + options, + schemaController ); }) .then(() => { // Notify LiveQuery server if possible - config.database.loadSchema().then(schemaController => { - const perms = schemaController.getClassLevelPermissions(className); - config.liveQueryController.onAfterDelete( - className, - inflatedObject, - null, - perms - ); - }); + const perms = schemaController.getClassLevelPermissions(className); + config.liveQueryController.onAfterDelete( + className, + inflatedObject, + null, + perms + ); return triggers.maybeRunTrigger( triggers.Types.afterDelete, auth,