Schema Cache Improvement 2 (#5616)

* schema hasClass improvement

* create object improvement

* destroy object

* update object

* hasClass test rewrite

* more tests

* improve signing up users
This commit is contained in:
Diamond Lewis
2019-05-30 11:14:05 -05:00
committed by GitHub
parent 9f226a254a
commit cc6d474dcb
8 changed files with 572 additions and 405 deletions

View File

@@ -175,20 +175,20 @@ describe_only(() => {
beforeEach(async () => { beforeEach(async () => {
await cacheAdapter.clear(); await cacheAdapter.clear();
getSpy = spyOn(cacheAdapter, 'get').and.callThrough();
putSpy = spyOn(cacheAdapter, 'put').and.callThrough();
await reconfigureServer({ await reconfigureServer({
cacheAdapter, cacheAdapter,
enableSingleSchemaCache: true, enableSingleSchemaCache: true,
}); });
getSpy = spyOn(cacheAdapter, 'get').and.callThrough();
putSpy = spyOn(cacheAdapter, 'put').and.callThrough();
}); });
it('test new object', async () => { it('test new object', async () => {
const object = new TestObject(); const object = new TestObject();
object.set('foo', 'bar'); object.set('foo', 'bar');
await object.save(); await object.save();
expect(getSpy.calls.count()).toBe(4); expect(getSpy.calls.count()).toBe(2);
expect(putSpy.calls.count()).toBe(3); expect(putSpy.calls.count()).toBe(2);
}); });
it('test new object multiple fields', async () => { it('test new object multiple fields', async () => {
@@ -200,8 +200,8 @@ describe_only(() => {
booleanField: true, booleanField: true,
}); });
await container.save(); await container.save();
expect(getSpy.calls.count()).toBe(4); expect(getSpy.calls.count()).toBe(2);
expect(putSpy.calls.count()).toBe(3); expect(putSpy.calls.count()).toBe(2);
}); });
it('test update existing fields', async () => { it('test update existing fields', async () => {
@@ -214,7 +214,57 @@ describe_only(() => {
object.set('foo', 'barz'); object.set('foo', 'barz');
await object.save(); 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); expect(putSpy.calls.count()).toBe(0);
}); });
@@ -228,7 +278,7 @@ describe_only(() => {
object.set('new', 'barz'); object.set('new', 'barz');
await object.save(); await object.save();
expect(getSpy.calls.count()).toBe(3); expect(getSpy.calls.count()).toBe(2);
expect(putSpy.calls.count()).toBe(1); expect(putSpy.calls.count()).toBe(1);
}); });
@@ -248,8 +298,43 @@ describe_only(() => {
booleanField: true, booleanField: true,
}); });
await object.save(); 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(getSpy.calls.count()).toBe(3);
expect(putSpy.calls.count()).toBe(1); 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 () => { it('test query', async () => {
@@ -266,6 +351,45 @@ describe_only(() => {
expect(putSpy.calls.count()).toBe(0); 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 () => { it('test delete object', async () => {
const object = new TestObject(); const object = new TestObject();
object.set('foo', 'bar'); object.set('foo', 'bar');
@@ -275,7 +399,7 @@ describe_only(() => {
putSpy.calls.reset(); putSpy.calls.reset();
await object.destroy(); await object.destroy();
expect(getSpy.calls.count()).toBe(3); expect(getSpy.calls.count()).toBe(2);
expect(putSpy.calls.count()).toBe(0); expect(putSpy.calls.count()).toBe(0);
}); });

View File

@@ -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, { const customConfig = Object.assign({}, config, {
allowClientClassCreation: false, allowClientClassCreation: false,
}); });
config.database const schema = await config.database.loadSchema();
.loadSchema() const actualSchema = await schema.addClassIfNotExists(
.then(schema => schema.addClassIfNotExists('ClientClassCreation', {})) 'ClientClassCreation',
.then(actualSchema => { {}
expect(actualSchema.className).toEqual('ClientClassCreation'); );
return rest.find( expect(actualSchema.className).toEqual('ClientClassCreation');
customConfig,
auth.nobody(customConfig), await schema.reloadData({ clearCache: true });
'ClientClassCreation', // Should not throw
{} const result = await rest.find(
); customConfig,
}) auth.nobody(customConfig),
.then( 'ClientClassCreation',
result => { {}
expect(result.results.length).toEqual(0); );
done(); expect(result.results.length).toEqual(0);
},
() => {
fail('Should not throw error');
}
);
}); });
it('query with wrongly encoded parameter', done => { it('query with wrongly encoded parameter', done => {

View File

@@ -929,6 +929,7 @@ describe('SchemaController', () => {
.then(schema => { .then(schema => {
return schema return schema
.addClassIfNotExists('NewClass', {}) .addClassIfNotExists('NewClass', {})
.then(() => schema.reloadData({ clearCache: true }))
.then(() => { .then(() => {
schema schema
.hasClass('NewClass') .hasClass('NewClass')

View File

@@ -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, { const customConfig = Object.assign({}, config, {
allowClientClassCreation: false, allowClientClassCreation: false,
}); });
config.database const schema = await config.database.loadSchema();
.loadSchema() const actualSchema = await schema.addClassIfNotExists(
.then(schema => schema.addClassIfNotExists('ClientClassCreation', {})) 'ClientClassCreation',
.then(actualSchema => { {}
expect(actualSchema.className).toEqual('ClientClassCreation'); );
return rest.create( expect(actualSchema.className).toEqual('ClientClassCreation');
customConfig,
auth.nobody(customConfig), await schema.reloadData({ clearCache: true });
'ClientClassCreation', // Should not throw
{} await rest.create(
); customConfig,
}) auth.nobody(customConfig),
.then( 'ClientClassCreation',
() => { {}
done(); );
},
() => {
fail('Should not throw error');
}
);
}); });
it('handles user signup', done => { it('handles user signup', done => {

View File

@@ -432,6 +432,15 @@ class DatabaseController {
return this.loadSchema(options); return this.loadSchema(options);
} }
loadSchemaIfNeeded(
schemaController: SchemaController.SchemaController,
options: LoadSchemaOptions = { clearCache: false }
): Promise<SchemaController.SchemaController> {
return schemaController
? Promise.resolve(schemaController)
: this.loadSchema(options);
}
// Returns a promise for the classname that is related to the given // Returns a promise for the classname that is related to the given
// classname through the key. // classname through the key.
// TODO: make this not in the DatabaseController interface // TODO: make this not in the DatabaseController interface
@@ -477,7 +486,8 @@ class DatabaseController {
update: any, update: any,
{ acl, many, upsert }: FullQueryOptions = {}, { acl, many, upsert }: FullQueryOptions = {},
skipSanitization: boolean = false, skipSanitization: boolean = false,
validateOnly: boolean = false validateOnly: boolean = false,
validSchemaController: SchemaController.SchemaController
): Promise<any> { ): Promise<any> {
const originalQuery = query; const originalQuery = query;
const originalUpdate = update; const originalUpdate = update;
@@ -486,141 +496,145 @@ class DatabaseController {
var relationUpdates = []; var relationUpdates = [];
var isMaster = acl === undefined; var isMaster = acl === undefined;
var aclGroup = acl || []; var aclGroup = acl || [];
return this.loadSchema().then(schemaController => {
return (isMaster return this.loadSchemaIfNeeded(validSchemaController).then(
? Promise.resolve() schemaController => {
: schemaController.validatePermission(className, aclGroup, 'update') return (isMaster
) ? Promise.resolve()
.then(() => { : schemaController.validatePermission(className, aclGroup, 'update')
relationUpdates = this.collectRelationUpdates( )
className, .then(() => {
originalQuery.objectId, relationUpdates = this.collectRelationUpdates(
update
);
if (!isMaster) {
query = this.addPointerPermissions(
schemaController,
className, className,
'update', originalQuery.objectId,
query, update
aclGroup
); );
} if (!isMaster) {
if (!query) { query = this.addPointerPermissions(
return Promise.resolve(); schemaController,
} className,
if (acl) { 'update',
query = addWriteACL(query, acl); query,
} aclGroup
validateQuery(query); );
return schemaController }
.getOneSchema(className, true) if (!query) {
.catch(error => { return Promise.resolve();
// If the schema doesn't exist, pretend it exists with no fields. This behavior }
// will likely need revisiting. if (acl) {
if (error === undefined) { query = addWriteACL(query, acl);
return { fields: {} }; }
} validateQuery(query);
throw error; return schemaController
}) .getOneSchema(className, true)
.then(schema => { .catch(error => {
Object.keys(update).forEach(fieldName => { // If the schema doesn't exist, pretend it exists with no fields. This behavior
if (fieldName.match(/^authData\.([a-zA-Z0-9_]+)\.id$/)) { // will likely need revisiting.
throw new Parse.Error( if (error === undefined) {
Parse.Error.INVALID_KEY_NAME, return { fields: {} };
`Invalid field name for update: ${fieldName}`
);
} }
const rootFieldName = getRootFieldName(fieldName); throw error;
if ( })
!SchemaController.fieldNameIsValid(rootFieldName) && .then(schema => {
!isSpecialUpdateKey(rootFieldName) Object.keys(update).forEach(fieldName => {
) { if (fieldName.match(/^authData\.([a-zA-Z0-9_]+)\.id$/)) {
throw new Parse.Error( throw new Parse.Error(
Parse.Error.INVALID_KEY_NAME, Parse.Error.INVALID_KEY_NAME,
`Invalid field name for update: ${fieldName}` `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 ( .then((result: any) => {
update[updateOperation] && if (!result) {
typeof update[updateOperation] === 'object' && throw new Parse.Error(
Object.keys(update[updateOperation]).some( Parse.Error.OBJECT_NOT_FOUND,
innerKey => innerKey.includes('$') || innerKey.includes('.') 'Object not found.'
) );
) { }
throw new Parse.Error( if (validateOnly) {
Parse.Error.INVALID_NESTED_KEY, return result;
"Nested keys should not contain the '$' or '.' characters" }
); return this.handleRelationUpdates(
} className,
} originalQuery.objectId,
update = transformObjectACL(update); update,
transformAuthData(className, update, schema); relationUpdates
if (validateOnly) { ).then(() => {
return this.adapter return result;
.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) => { .then(result => {
if (!result) { if (skipSanitization) {
throw new Parse.Error( return Promise.resolve(result);
Parse.Error.OBJECT_NOT_FOUND, }
'Object not found.' return sanitizeDatabaseResult(originalUpdate, result);
);
}
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);
});
});
} }
// Collect all relation-updating operations from a REST-format update. // Collect all relation-updating operations from a REST-format update.
@@ -753,65 +767,68 @@ class DatabaseController {
destroy( destroy(
className: string, className: string,
query: any, query: any,
{ acl }: QueryOptions = {} { acl }: QueryOptions = {},
validSchemaController: SchemaController.SchemaController
): Promise<any> { ): Promise<any> {
const isMaster = acl === undefined; const isMaster = acl === undefined;
const aclGroup = acl || []; const aclGroup = acl || [];
return this.loadSchema().then(schemaController => { return this.loadSchemaIfNeeded(validSchemaController).then(
return (isMaster schemaController => {
? Promise.resolve() return (isMaster
: schemaController.validatePermission(className, aclGroup, 'delete') ? Promise.resolve()
).then(() => { : schemaController.validatePermission(className, aclGroup, 'delete')
if (!isMaster) { ).then(() => {
query = this.addPointerPermissions( if (!isMaster) {
schemaController, query = this.addPointerPermissions(
className, schemaController,
'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(
className, className,
parseFormatSchema, 'delete',
query query,
) aclGroup
) );
.catch(error => { if (!query) {
// When deleting sessions while changing passwords, don't throw an error if they don't have any sessions. throw new Parse.Error(
if ( Parse.Error.OBJECT_NOT_FOUND,
className === '_Session' && 'Object not found.'
error.code === Parse.Error.OBJECT_NOT_FOUND );
) {
return Promise.resolve({});
} }
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. // Inserts an object into the database.
@@ -820,7 +837,8 @@ class DatabaseController {
className: string, className: string,
object: any, object: any,
{ acl }: QueryOptions = {}, { acl }: QueryOptions = {},
validateOnly: boolean = false validateOnly: boolean = false,
validSchemaController: SchemaController.SchemaController
): Promise<any> { ): Promise<any> {
// Make a copy of the object, so we don't mutate the incoming data. // Make a copy of the object, so we don't mutate the incoming data.
const originalObject = object; const originalObject = object;
@@ -836,8 +854,9 @@ class DatabaseController {
null, null,
object object
); );
return this.validateClassName(className) return this.validateClassName(className)
.then(() => this.loadSchema()) .then(() => this.loadSchemaIfNeeded(validSchemaController))
.then(schemaController => { .then(schemaController => {
return (isMaster return (isMaster
? Promise.resolve() ? Promise.resolve()
@@ -1173,7 +1192,8 @@ class DatabaseController {
pipeline, pipeline,
readPreference, readPreference,
}: any = {}, }: any = {},
auth: any = {} auth: any = {},
validSchemaController: SchemaController.SchemaController
): Promise<any> { ): Promise<any> {
const isMaster = acl === undefined; const isMaster = acl === undefined;
const aclGroup = acl || []; const aclGroup = acl || [];
@@ -1186,153 +1206,157 @@ class DatabaseController {
op = count === true ? 'count' : op; op = count === true ? 'count' : op;
let classExists = true; let classExists = true;
return this.loadSchema().then(schemaController => { return this.loadSchemaIfNeeded(validSchemaController).then(
//Allow volatile classes if querying with Master (for _PushStatus) schemaController => {
//TODO: Move volatile classes concept into mongo adapter, postgres adapter shouldn't care //Allow volatile classes if querying with Master (for _PushStatus)
//that api.parse.com breaks when _PushStatus exists in mongo. //TODO: Move volatile classes concept into mongo adapter, postgres adapter shouldn't care
return schemaController //that api.parse.com breaks when _PushStatus exists in mongo.
.getOneSchema(className, isMaster) return schemaController
.catch(error => { .getOneSchema(className, isMaster)
// Behavior for non-existent classes is kinda weird on Parse.com. Probably doesn't matter too much. .catch(error => {
// For now, pretend the class exists but has no objects, // Behavior for non-existent classes is kinda weird on Parse.com. Probably doesn't matter too much.
if (error === undefined) { // For now, pretend the class exists but has no objects,
classExists = false; if (error === undefined) {
return { fields: {} }; 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}`
);
} }
const rootFieldName = getRootFieldName(fieldName); throw error;
if (!SchemaController.fieldNameIsValid(rootFieldName)) { })
throw new Parse.Error( .then(schema => {
Parse.Error.INVALID_KEY_NAME, // Parse.com treats queries on _created_at and _updated_at as if they were queries on createdAt and updatedAt,
`Invalid field name: ${fieldName}.` // 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) {
return (isMaster sort.updatedAt = sort._updated_at;
? Promise.resolve() delete sort._updated_at;
: schemaController.validatePermission(className, aclGroup, op) }
) const queryOptions = { skip, limit, sort, keys, readPreference };
.then(() => this.reduceRelationKeys(className, query, queryOptions)) Object.keys(sort).forEach(fieldName => {
.then(() => if (fieldName.match(/^authData\.([a-zA-Z0-9_]+)\.id$/)) {
this.reduceInRelation(className, query, schemaController) throw new Parse.Error(
) Parse.Error.INVALID_KEY_NAME,
.then(() => { `Cannot sort by ${fieldName}`
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) { const rootFieldName = getRootFieldName(fieldName);
if (op === 'get') { if (!SchemaController.fieldNameIsValid(rootFieldName)) {
throw new Parse.Error( throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND, Parse.Error.INVALID_KEY_NAME,
'Object not found.' `Invalid field name: ${fieldName}.`
); );
} 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
);
});
} }
}); });
}); 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<void> { deleteSchema(className: string): Promise<void> {

View File

@@ -646,7 +646,7 @@ export default class SchemaController {
fields: SchemaFields = {}, fields: SchemaFields = {},
classLevelPermissions: any, classLevelPermissions: any,
indexes: any = {} indexes: any = {}
): Promise<void> { ): Promise<void | Schema> {
var validationError = this.validateNewClass( var validationError = this.validateNewClass(
className, className,
fields, fields,
@@ -667,11 +667,6 @@ export default class SchemaController {
}) })
) )
.then(convertAdapterSchemaToParseSchema) .then(convertAdapterSchemaToParseSchema)
.then(res => {
return this._cache.clear().then(() => {
return Promise.resolve(res);
});
})
.catch(error => { .catch(error => {
if (error && error.code === Parse.Error.DUPLICATE_VALUE) { if (error && error.code === Parse.Error.DUPLICATE_VALUE) {
throw new Parse.Error( throw new Parse.Error(
@@ -1285,6 +1280,9 @@ export default class SchemaController {
// Checks if a given class is in the schema. // Checks if a given class is in the schema.
hasClass(className: string) { hasClass(className: string) {
if (this.schemaData[className]) {
return Promise.resolve(true);
}
return this.reloadData().then(() => !!this.schemaData[className]); return this.reloadData().then(() => !!this.schemaData[className]);
} }
} }

View File

@@ -69,6 +69,10 @@ function RestWrite(
// The timestamp we'll use for this whole operation // The timestamp we'll use for this whole operation
this.updatedAt = Parse._encode(new Date()).iso; 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 // A convenient method to perform all the steps of processing the
@@ -101,7 +105,8 @@ RestWrite.prototype.execute = function() {
.then(() => { .then(() => {
return this.validateSchema(); return this.validateSchema();
}) })
.then(() => { .then(schemaController => {
this.validSchemaController = schemaController;
return this.setRequiredFieldsIfNeeded(); return this.setRequiredFieldsIfNeeded();
}) })
.then(() => { .then(() => {
@@ -614,7 +619,9 @@ RestWrite.prototype._validateUserName = function() {
.find( .find(
this.className, this.className,
{ username: this.data.username, objectId: { $ne: this.objectId() } }, { username: this.data.username, objectId: { $ne: this.objectId() } },
{ limit: 1 } { limit: 1 },
{},
this.validSchemaController
) )
.then(results => { .then(results => {
if (results.length > 0) { if (results.length > 0) {
@@ -645,7 +652,9 @@ RestWrite.prototype._validateEmail = function() {
.find( .find(
this.className, this.className,
{ email: this.data.email, objectId: { $ne: this.objectId() } }, { email: this.data.email, objectId: { $ne: this.objectId() } },
{ limit: 1 } { limit: 1 },
{},
this.validSchemaController
) )
.then(results => { .then(results => {
if (results.length > 0) { if (results.length > 0) {
@@ -854,11 +863,16 @@ RestWrite.prototype.destroyDuplicatedSessions = function() {
if (!user.objectId) { if (!user.objectId) {
return; return;
} }
this.config.database.destroy('_Session', { this.config.database.destroy(
user, '_Session',
installationId, {
sessionToken: { $ne: sessionToken }, user,
}); installationId,
sessionToken: { $ne: sessionToken },
},
{},
this.validSchemaController
);
}; };
// Handles any followup logic // Handles any followup logic
@@ -1361,7 +1375,15 @@ RestWrite.prototype.runDatabaseOperation = function() {
return defer.then(() => { return defer.then(() => {
// Run an update // Run an update
return this.config.database 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 => { .then(response => {
response.updatedAt = this.updatedAt; response.updatedAt = this.updatedAt;
this._updateResponseWithData(response, this.data); this._updateResponseWithData(response, this.data);
@@ -1391,7 +1413,13 @@ RestWrite.prototype.runDatabaseOperation = function() {
// Run a create // Run a create
return this.config.database return this.config.database
.create(this.className, this.data, this.runOptions) .create(
this.className,
this.data,
this.runOptions,
false,
this.validSchemaController
)
.catch(error => { .catch(error => {
if ( if (
this.className !== '_User' || this.className !== '_User' ||

View File

@@ -102,6 +102,7 @@ function del(config, auth, className, objectId) {
enforceRoleSecurity('delete', className, auth); enforceRoleSecurity('delete', className, auth);
let inflatedObject; let inflatedObject;
let schemaController;
return Promise.resolve() return Promise.resolve()
.then(() => { .then(() => {
@@ -151,8 +152,10 @@ function del(config, auth, className, objectId) {
return; return;
} }
}) })
.then(() => { .then(() => config.database.loadSchema())
var options = {}; .then(s => {
schemaController = s;
const options = {};
if (!auth.isMaster) { if (!auth.isMaster) {
options.acl = ['*']; options.acl = ['*'];
if (auth.user) { if (auth.user) {
@@ -166,20 +169,19 @@ function del(config, auth, className, objectId) {
{ {
objectId: objectId, objectId: objectId,
}, },
options options,
schemaController
); );
}) })
.then(() => { .then(() => {
// Notify LiveQuery server if possible // Notify LiveQuery server if possible
config.database.loadSchema().then(schemaController => { const perms = schemaController.getClassLevelPermissions(className);
const perms = schemaController.getClassLevelPermissions(className); config.liveQueryController.onAfterDelete(
config.liveQueryController.onAfterDelete( className,
className, inflatedObject,
inflatedObject, null,
null, perms
perms );
);
});
return triggers.maybeRunTrigger( return triggers.maybeRunTrigger(
triggers.Types.afterDelete, triggers.Types.afterDelete,
auth, auth,