Merge pull request #1644 from drew-gross/refactor-query-transform

Break dependency of deleteObjectsByQuery on schemaController
This commit is contained in:
Fosco Marotto
2016-05-09 11:21:22 -07:00
10 changed files with 195 additions and 149 deletions

View File

@@ -106,7 +106,7 @@ describe('parseObjectToMongoObjectForCreate', () => {
describe('transformWhere', () => { describe('transformWhere', () => {
it('objectId', (done) => { it('objectId', (done) => {
var out = transform.transformWhere(dummySchema, null, {objectId: 'foo'}); var out = transform.transformWhere(null, {objectId: 'foo'});
expect(out._id).toEqual('foo'); expect(out._id).toEqual('foo');
done(); done();
}); });
@@ -115,7 +115,7 @@ describe('transformWhere', () => {
var input = { var input = {
objectId: {'$in': ['one', 'two', 'three']}, objectId: {'$in': ['one', 'two', 'three']},
}; };
var output = transform.transformWhere(dummySchema, null, input); var output = transform.transformWhere(null, input);
jequal(input.objectId, output._id); jequal(input.objectId, output._id);
done(); done();
}); });

View File

@@ -76,7 +76,7 @@ describe('Hooks', () => {
}) })
}); });
it("should CRUD a trigger registration", (done) => { it("should CRUD a trigger registration", (done) => {
// Create // Create
Parse.Hooks.createTrigger("MyClass","beforeDelete", "http://someurl").then((res) => { Parse.Hooks.createTrigger("MyClass","beforeDelete", "http://someurl").then((res) => {
expect(res.className).toBe("MyClass"); expect(res.className).toBe("MyClass");

View File

@@ -124,7 +124,6 @@ describe('SchemaController', () => {
var obj; var obj;
createTestUser() createTestUser()
.then(user => { .then(user => {
console.log(user);
return config.database.loadSchema() return config.database.loadSchema()
// Create a valid class // Create a valid class
.then(schema => schema.validateObject('Stuff', {foo: 'bar'})) .then(schema => schema.validateObject('Stuff', {foo: 'bar'}))

View File

@@ -173,16 +173,11 @@ export class MongoStorageAdapter {
// If no objects match, reject with OBJECT_NOT_FOUND. If objects are found and deleted, resolve with undefined. // If no objects match, reject with OBJECT_NOT_FOUND. If objects are found and deleted, resolve with undefined.
// If there is some other error, reject with INTERNAL_SERVER_ERROR. // If there is some other error, reject with INTERNAL_SERVER_ERROR.
// Currently accepts the schemaController, and validate for lecacy reasons // Currently accepts validate for legacy reasons. Currently accepts the schema, that may not actually be necessary.
deleteObjectsByQuery(className, query, schemaController, validate) { deleteObjectsByQuery(className, query, validate, schema) {
return this.adaptiveCollection(className) return this.adaptiveCollection(className)
.then(collection => { .then(collection => {
let mongoWhere = transform.transformWhere( let mongoWhere = transform.transformWhere(className, query, { validate }, schema);
schemaController,
className,
query,
{ validate }
);
return collection.deleteMany(mongoWhere) return collection.deleteMany(mongoWhere)
}) })
.then(({ result }) => { .then(({ result }) => {

View File

@@ -11,9 +11,6 @@ var Parse = require('parse/node').Parse;
// //
// There are several options that can help transform: // There are several options that can help transform:
// //
// query: true indicates that query constraints like $lt are allowed in
// the value.
//
// update: true indicates that __op operators like Add and Delete // update: true indicates that __op operators like Add and Delete
// in the value are converted to a mongo update form. Otherwise they are // in the value are converted to a mongo update form. Otherwise they are
// converted to static data. // converted to static data.
@@ -21,10 +18,9 @@ var Parse = require('parse/node').Parse;
// validate: true indicates that key names are to be validated. // validate: true indicates that key names are to be validated.
// //
// Returns an object with {key: key, value: value}. // Returns an object with {key: key, value: value}.
export function transformKeyValue(schema, className, restKey, restValue, { function transformKeyValue(schema, className, restKey, restValue, {
inArray, inArray,
inObject, inObject,
query,
update, update,
validate, validate,
} = {}) { } = {}) {
@@ -66,47 +62,17 @@ export function transformKeyValue(schema, className, restKey, restValue, {
return {key: key, value: restValue}; return {key: key, value: restValue};
break; break;
case '$or': case '$or':
if (!query) { throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'you can only use $or in queries');
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME,
'you can only use $or in queries');
}
if (!(restValue instanceof Array)) {
throw new Parse.Error(Parse.Error.INVALID_QUERY,
'bad $or format - use an array value');
}
var mongoSubqueries = restValue.map((s) => {
return transformWhere(schema, className, s);
});
return {key: '$or', value: mongoSubqueries};
case '$and': case '$and':
if (!query) { throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'you can only use $and in queries');
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME,
'you can only use $and in queries');
}
if (!(restValue instanceof Array)) {
throw new Parse.Error(Parse.Error.INVALID_QUERY,
'bad $and format - use an array value');
}
var mongoSubqueries = restValue.map((s) => {
return transformWhere(schema, className, s);
});
return {key: '$and', value: mongoSubqueries};
default: default:
// Other auth data // Other auth data
var authDataMatch = key.match(/^authData\.([a-zA-Z0-9_]+)\.id$/); var authDataMatch = key.match(/^authData\.([a-zA-Z0-9_]+)\.id$/);
if (authDataMatch) { if (authDataMatch) {
if (query) { throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'can only query on ' + key);
var provider = authDataMatch[1]; }
// Special-case auth data.
return {key: '_auth_data_'+provider+'.id', value: restValue};
}
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME,
'can only query on ' + key);
break;
};
if (validate && !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) { if (validate && !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) {
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'invalid key name: ' + key);
'invalid key name: ' + key);
} }
} }
@@ -123,20 +89,6 @@ export function transformKeyValue(schema, className, restKey, restValue, {
} }
var expectedTypeIsArray = (expected && expected.type === 'Array'); var expectedTypeIsArray = (expected && expected.type === 'Array');
// Handle query constraints
if (query) {
value = transformConstraint(restValue, expectedTypeIsArray);
if (value !== CannotTransform) {
return {key: key, value: value};
}
}
if (expectedTypeIsArray && query && !(restValue instanceof Array)) {
return {
key: key, value: { '$all' : [restValue] }
};
}
// Handle atomic values // Handle atomic values
var value = transformAtom(restValue, false, { inArray, inObject }); var value = transformAtom(restValue, false, { inArray, inObject });
if (value !== CannotTransform) { if (value !== CannotTransform) {
@@ -154,10 +106,6 @@ export function transformKeyValue(schema, className, restKey, restValue, {
// Handle arrays // Handle arrays
if (restValue instanceof Array) { if (restValue instanceof Array) {
if (query) {
throw new Parse.Error(Parse.Error.INVALID_JSON,
'cannot use array as query param');
}
value = restValue.map((restObj) => { value = restValue.map((restObj) => {
var out = transformKeyValue(schema, className, restKey, restObj, { inArray: true }); var out = transformKeyValue(schema, className, restKey, restObj, { inArray: true });
return out.value; return out.value;
@@ -182,20 +130,105 @@ export function transformKeyValue(schema, className, restKey, restValue, {
return {key: key, value: value}; return {key: key, value: value};
} }
const valueAsDate = value => {
if (typeof value === 'string') {
return new Date(value);
} else if (value instanceof Date) {
return value;
}
return false;
}
function transformQueryKeyValue(className, key, value, { validate } = {}, schema) {
switch(key) {
case 'createdAt':
if (valueAsDate(value)) {
return {key: '_created_at', value: valueAsDate(value)}
}
key = '_created_at';
break;
case 'updatedAt':
if (valueAsDate(value)) {
return {key: '_updated_at', value: valueAsDate(value)}
}
key = '_updated_at';
break;
case 'expiresAt':
if (valueAsDate(value)) {
return {key: 'expiresAt', value: valueAsDate(value)}
}
break;
case 'objectId': return {key: '_id', value}
case 'sessionToken': return {key: '_session_token', value}
case '_rperm':
case '_wperm':
case '_perishable_token':
case '_email_verify_token': return {key, value}
case '$or':
if (!(value instanceof Array)) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'bad $or format - use an array value');
}
return {key: '$or', value: value.map(subQuery => transformWhere(className, subQuery, {}, schema))};
case '$and':
if (!(value instanceof Array)) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'bad $and format - use an array value');
}
return {key: '$and', value: value.map(subQuery => transformWhere(className, subQuery, {}, schema))};
default:
// Other auth data
const authDataMatch = key.match(/^authData\.([a-zA-Z0-9_]+)\.id$/);
if (authDataMatch) {
const provider = authDataMatch[1];
// Special-case auth data.
return {key: `_auth_data_${provider}.id`, value};
}
if (validate && !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) {
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'invalid key name: ' + key);
}
}
const expectedTypeIsArray =
schema &&
schema.fields[key] &&
schema.fields[key].type === 'Array';
const expectedTypeIsPointer =
schema &&
schema.fields[key] &&
schema.fields[key].type === 'Pointer';
if (expectedTypeIsPointer || !schema && value && value.__type === 'Pointer') {
key = '_p_' + key;
}
// Handle query constraints
if (transformConstraint(value, expectedTypeIsArray) !== CannotTransform) {
return {key, value: transformConstraint(value, expectedTypeIsArray)};
}
if (expectedTypeIsArray && !(value instanceof Array)) {
return {key, value: { '$all' : [value] }};
}
// Handle atomic values
if (transformAtom(value, false) !== CannotTransform) {
return {key, value: transformAtom(value, false)};
} else {
throw new Parse.Error(Parse.Error.INVALID_JSON, `You cannot use ${value} as a query parameter.`);
}
}
// Main exposed method to help run queries. // Main exposed method to help run queries.
// restWhere is the "where" clause in REST API form. // restWhere is the "where" clause in REST API form.
// Returns the mongo form of the query. // Returns the mongo form of the query.
// Throws a Parse.Error if the input query is invalid. // Throws a Parse.Error if the input query is invalid.
function transformWhere(schema, className, restWhere, options = {validate: true}) { function transformWhere(className, restWhere, { validate = true } = {}, schema) {
let mongoWhere = {}; let mongoWhere = {};
if (restWhere['ACL']) { if (restWhere['ACL']) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.'); throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.');
} }
let transformKeyOptions = {query: true};
transformKeyOptions.validate = options.validate;
for (let restKey in restWhere) { for (let restKey in restWhere) {
let out = transformKeyValue(schema, className, restKey, restWhere[restKey], transformKeyOptions); let out = transformQueryKeyValue(className, restKey, restWhere[restKey], { validate }, schema);
mongoWhere[out.key] = out.value; mongoWhere[out.key] = out.value;
} }
return mongoWhere; return mongoWhere;

View File

@@ -52,11 +52,7 @@ var getAuthForSessionToken = function({ config, sessionToken, installationId } =
limit: 1, limit: 1,
include: 'user' include: 'user'
}; };
var restWhere = { var query = new RestQuery(config, master(config), '_Session', { sessionToken }, restOptions);
_session_token: sessionToken
};
var query = new RestQuery(config, master(config), '_Session',
restWhere, restOptions);
return query.execute().then((response) => { return query.execute().then((response) => {
var results = response.results; var results = response.results;
if (results.length !== 1 || !results[0]['user']) { if (results.length !== 1 || !results[0]['user']) {

View File

@@ -158,20 +158,15 @@ DatabaseController.prototype.update = function(className, query, update, {
var isMaster = acl === undefined; var isMaster = acl === undefined;
var aclGroup = acl || []; var aclGroup = acl || [];
var mongoUpdate, schema; var mongoUpdate;
return this.loadSchema() return this.loadSchema()
.then(s => { .then(schemaController => {
schema = s; return (isMaster ? Promise.resolve() : schemaController.validatePermission(className, aclGroup, 'update'))
if (!isMaster) {
return schema.validatePermission(className, aclGroup, 'update');
}
return Promise.resolve();
})
.then(() => this.handleRelationUpdates(className, query.objectId, update)) .then(() => this.handleRelationUpdates(className, query.objectId, update))
.then(() => this.adapter.adaptiveCollection(className)) .then(() => this.adapter.adaptiveCollection(className))
.then(collection => { .then(collection => {
if (!isMaster) { if (!isMaster) {
query = this.addPointerPermissions(schema, className, 'update', query, aclGroup); query = this.addPointerPermissions(schemaController, className, 'update', query, aclGroup);
} }
if (!query) { if (!query) {
return Promise.resolve(); return Promise.resolve();
@@ -179,26 +174,42 @@ DatabaseController.prototype.update = function(className, query, update, {
if (acl) { if (acl) {
query = addWriteACL(query, acl); query = addWriteACL(query, acl);
} }
var mongoWhere = this.transform.transformWhere(schema, className, query, {validate: !this.skipValidation}); return schemaController.getOneSchema(className)
mongoUpdate = this.transform.transformUpdate(schema, className, update, {validate: !this.skipValidation}); .catch(error => {
if (many) { // If the schema doesn't exist, pretend it exists with no fields. This behaviour
return collection.updateMany(mongoWhere, mongoUpdate); // will likely need revisiting.
} else if (upsert) { if (error === undefined) {
return collection.upsertOne(mongoWhere, mongoUpdate); return { fields: {} };
} else { }
return collection.findOneAndUpdate(mongoWhere, mongoUpdate); throw error;
} })
.then(parseFormatSchema => {
var mongoWhere = this.transform.transformWhere(className, query, {validate: !this.skipValidation}, parseFormatSchema);
mongoUpdate = this.transform.transformUpdate(
schemaController,
className,
update,
{validate: !this.skipValidation}
);
if (many) {
return collection.updateMany(mongoWhere, mongoUpdate);
} else if (upsert) {
return collection.upsertOne(mongoWhere, mongoUpdate);
} else {
return collection.findOneAndUpdate(mongoWhere, mongoUpdate);
}
});
}) })
.then(result => { .then(result => {
if (!result) { if (!result) {
return Promise.reject(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, return Promise.reject(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.'));
'Object not found.'));
} }
if (this.skipValidation) { if (this.skipValidation) {
return Promise.resolve(result); return Promise.resolve(result);
} }
return sanitizeDatabaseResult(originalUpdate, result); return sanitizeDatabaseResult(originalUpdate, result);
}); });
});
}; };
function sanitizeDatabaseResult(originalObject, result) { function sanitizeDatabaseResult(originalObject, result) {
@@ -317,7 +328,16 @@ DatabaseController.prototype.destroy = function(className, query, { acl } = {})
if (acl) { if (acl) {
query = addWriteACL(query, acl); query = addWriteACL(query, acl);
} }
return this.adapter.deleteObjectsByQuery(className, query, schemaController, !this.skipValidation) return schemaController.getOneSchema(className)
.catch(error => {
// If the schema doesn't exist, pretend it exists with no fields. This behaviour
// will likely need revisiting.
if (error === undefined) {
return { fields: {} };
}
throw error;
})
.then(parseFormatSchema => this.adapter.deleteObjectsByQuery(className, query, !this.skipValidation, parseFormatSchema))
.catch(error => { .catch(error => {
// When deleting sessions while changing passwords, don't throw an error if they don't have any sessions. // 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) { if (className === "_Session" && error.code === Parse.Error.OBJECT_NOT_FOUND) {
@@ -593,56 +613,59 @@ DatabaseController.prototype.find = function(className, query, {
} }
let isMaster = acl === undefined; let isMaster = acl === undefined;
let aclGroup = acl || []; let aclGroup = acl || [];
let schema = null; let op = typeof query.objectId == 'string' && Object.keys(query).length === 1 ? 'get' : 'find';
let op = typeof query.objectId == 'string' && Object.keys(query).length === 1 ? return this.loadSchema()
'get' : .then(schemaController => {
'find';
return this.loadSchema().then(s => {
schema = s;
if (sort) { if (sort) {
mongoOptions.sort = {}; mongoOptions.sort = {};
for (let key in sort) { for (let key in sort) {
let mongoKey = this.transform.transformKey(schema, className, key); let mongoKey = this.transform.transformKey(schemaController, className, key);
mongoOptions.sort[mongoKey] = sort[key]; mongoOptions.sort[mongoKey] = sort[key];
} }
} }
return (isMaster ? Promise.resolve() : schemaController.validatePermission(className, aclGroup, op))
if (!isMaster) { .then(() => this.reduceRelationKeys(className, query))
return schema.validatePermission(className, aclGroup, op); .then(() => this.reduceInRelation(className, query, schemaController))
} .then(() => this.adapter.adaptiveCollection(className))
return Promise.resolve(); .then(collection => {
}) if (!isMaster) {
.then(() => this.reduceRelationKeys(className, query)) query = this.addPointerPermissions(schemaController, className, op, query, aclGroup);
.then(() => this.reduceInRelation(className, query, schema))
.then(() => this.adapter.adaptiveCollection(className))
.then(collection => {
if (!isMaster) {
query = this.addPointerPermissions(schema, className, op, query, aclGroup);
}
if (!query) {
if (op == 'get') {
return Promise.reject(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
'Object not found.'));
} else {
return Promise.resolve([]);
} }
} if (!query) {
if (!isMaster) { if (op == 'get') {
query = addReadACL(query, aclGroup); return Promise.reject(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
} 'Object not found.'));
let mongoWhere = this.transform.transformWhere(schema, className, query); } else {
if (count) { return Promise.resolve([]);
delete mongoOptions.limit; }
return collection.count(mongoWhere, mongoOptions); }
} else { if (!isMaster) {
return collection.find(mongoWhere, mongoOptions) query = addReadACL(query, aclGroup);
.then((mongoResults) => { }
return mongoResults.map((r) => { return schemaController.getOneSchema(className)
return this.untransformObject( .catch(error => {
schema, isMaster, aclGroup, className, r); // If the schema doesn't exist, pretend it exists with no fields. This behaviour
// will likely need revisiting.
if (error === undefined) {
return { fields: {} };
}
throw error;
})
.then(parseFormatSchema => {
let mongoWhere = this.transform.transformWhere(className, query, {}, parseFormatSchema);
if (count) {
delete mongoOptions.limit;
return collection.count(mongoWhere, mongoOptions);
} else {
return collection.find(mongoWhere, mongoOptions)
.then((mongoResults) => {
return mongoResults.map((r) => {
return this.untransformObject(schemaController, isMaster, aclGroup, className, r);
});
}); });
}); }
} });
});
}); });
}; };

View File

@@ -1,12 +1,12 @@
// global_config.js // global_config.js
import PromiseRouter from '../PromiseRouter'; import PromiseRouter from '../PromiseRouter';
import * as middleware from "../middlewares"; import * as middleware from "../middlewares";
export class GlobalConfigRouter extends PromiseRouter { export class GlobalConfigRouter extends PromiseRouter {
getGlobalConfig(req) { getGlobalConfig(req) {
let database = req.config.database.WithoutValidation(); let database = req.config.database.WithoutValidation();
return database.find('_GlobalConfig', { '_id': 1 }, { limit: 1 }).then((results) => { return database.find('_GlobalConfig', { objectId: 1 }, { limit: 1 }).then((results) => {
if (results.length != 1) { if (results.length != 1) {
// If there is no config in the database - return empty config. // If there is no config in the database - return empty config.
return { response: { params: {} } }; return { response: { params: {} } };

View File

@@ -1,8 +1,8 @@
import ClassesRouter from './ClassesRouter'; import ClassesRouter from './ClassesRouter';
import PromiseRouter from '../PromiseRouter'; import PromiseRouter from '../PromiseRouter';
import rest from '../rest'; import rest from '../rest';
import Auth from '../Auth'; import Auth from '../Auth';
export class SessionsRouter extends ClassesRouter { export class SessionsRouter extends ClassesRouter {
handleFind(req) { handleFind(req) {
@@ -36,7 +36,7 @@ export class SessionsRouter extends ClassesRouter {
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN,
'Session token required.'); 'Session token required.');
} }
return rest.find(req.config, Auth.master(req.config), '_Session', { _session_token: req.info.sessionToken }) return rest.find(req.config, Auth.master(req.config), '_Session', { sessionToken: req.info.sessionToken })
.then((response) => { .then((response) => {
if (!response.results || response.results.length == 0) { if (!response.results || response.results.length == 0) {
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN,

View File

@@ -46,7 +46,7 @@ export class UsersRouter extends ClassesRouter {
} }
let sessionToken = req.info.sessionToken; let sessionToken = req.info.sessionToken;
return rest.find(req.config, Auth.master(req.config), '_Session', return rest.find(req.config, Auth.master(req.config), '_Session',
{ _session_token: sessionToken }, { sessionToken },
{ include: 'user' }) { include: 'user' })
.then((response) => { .then((response) => {
if (!response.results || if (!response.results ||
@@ -139,7 +139,7 @@ export class UsersRouter extends ClassesRouter {
let success = {response: {}}; let success = {response: {}};
if (req.info && req.info.sessionToken) { if (req.info && req.info.sessionToken) {
return rest.find(req.config, Auth.master(req.config), '_Session', return rest.find(req.config, Auth.master(req.config), '_Session',
{ _session_token: req.info.sessionToken } { sessionToken: req.info.sessionToken }
).then((records) => { ).then((records) => {
if (records.results && records.results.length) { if (records.results && records.results.length) {
return rest.del(req.config, Auth.master(req.config), '_Session', return rest.del(req.config, Auth.master(req.config), '_Session',