diff --git a/spec/MongoTransform.spec.js b/spec/MongoTransform.spec.js index 59fee087b6..bae5805fad 100644 --- a/spec/MongoTransform.spec.js +++ b/spec/MongoTransform.spec.js @@ -106,7 +106,7 @@ describe('parseObjectToMongoObjectForCreate', () => { describe('transformWhere', () => { it('objectId', (done) => { - var out = transform.transformWhere(dummySchema, null, {objectId: 'foo'}); + var out = transform.transformWhere(null, {objectId: 'foo'}); expect(out._id).toEqual('foo'); done(); }); @@ -115,7 +115,7 @@ describe('transformWhere', () => { var input = { objectId: {'$in': ['one', 'two', 'three']}, }; - var output = transform.transformWhere(dummySchema, null, input); + var output = transform.transformWhere(null, input); jequal(input.objectId, output._id); done(); }); diff --git a/spec/ParseHooks.spec.js b/spec/ParseHooks.spec.js index 2a0cec5a72..d3c04f6900 100644 --- a/spec/ParseHooks.spec.js +++ b/spec/ParseHooks.spec.js @@ -76,7 +76,7 @@ describe('Hooks', () => { }) }); - it("should CRUD a trigger registration", (done) => { + it("should CRUD a trigger registration", (done) => { // Create Parse.Hooks.createTrigger("MyClass","beforeDelete", "http://someurl").then((res) => { expect(res.className).toBe("MyClass"); diff --git a/spec/Schema.spec.js b/spec/Schema.spec.js index 67367b4653..d824d2b048 100644 --- a/spec/Schema.spec.js +++ b/spec/Schema.spec.js @@ -124,7 +124,6 @@ describe('SchemaController', () => { var obj; createTestUser() .then(user => { - console.log(user); return config.database.loadSchema() // Create a valid class .then(schema => schema.validateObject('Stuff', {foo: 'bar'})) diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 803b3c63b7..d61df40cbc 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -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 there is some other error, reject with INTERNAL_SERVER_ERROR. - // Currently accepts the schemaController, and validate for lecacy reasons - deleteObjectsByQuery(className, query, schemaController, validate) { + // Currently accepts validate for legacy reasons. Currently accepts the schema, that may not actually be necessary. + deleteObjectsByQuery(className, query, validate, schema) { return this.adaptiveCollection(className) .then(collection => { - let mongoWhere = transform.transformWhere( - schemaController, - className, - query, - { validate } - ); + let mongoWhere = transform.transformWhere(className, query, { validate }, schema); return collection.deleteMany(mongoWhere) }) .then(({ result }) => { diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 34f0f15779..0013a39d0e 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -11,9 +11,6 @@ var Parse = require('parse/node').Parse; // // 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 // in the value are converted to a mongo update form. Otherwise they are // converted to static data. @@ -21,10 +18,9 @@ var Parse = require('parse/node').Parse; // validate: true indicates that key names are to be validated. // // Returns an object with {key: key, value: value}. -export function transformKeyValue(schema, className, restKey, restValue, { +function transformKeyValue(schema, className, restKey, restValue, { inArray, inObject, - query, update, validate, } = {}) { @@ -66,47 +62,17 @@ export function transformKeyValue(schema, className, restKey, restValue, { return {key: key, value: restValue}; break; case '$or': - if (!query) { - 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}; + throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'you can only use $or in queries'); case '$and': - if (!query) { - 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}; + throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'you can only use $and in queries'); default: // Other auth data var authDataMatch = key.match(/^authData\.([a-zA-Z0-9_]+)\.id$/); if (authDataMatch) { - if (query) { - 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; - }; + throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'can only query on ' + key); + } if (validate && !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) { - throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, - 'invalid key name: ' + key); + throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'invalid key name: ' + key); } } @@ -123,20 +89,6 @@ export function transformKeyValue(schema, className, restKey, restValue, { } 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 var value = transformAtom(restValue, false, { inArray, inObject }); if (value !== CannotTransform) { @@ -154,10 +106,6 @@ export function transformKeyValue(schema, className, restKey, restValue, { // Handle arrays if (restValue instanceof Array) { - if (query) { - throw new Parse.Error(Parse.Error.INVALID_JSON, - 'cannot use array as query param'); - } value = restValue.map((restObj) => { var out = transformKeyValue(schema, className, restKey, restObj, { inArray: true }); return out.value; @@ -182,20 +130,105 @@ export function transformKeyValue(schema, className, restKey, restValue, { 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. // restWhere is the "where" clause in REST API form. // Returns the mongo form of the query. // 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 = {}; if (restWhere['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) { - let out = transformKeyValue(schema, className, restKey, restWhere[restKey], transformKeyOptions); + let out = transformQueryKeyValue(className, restKey, restWhere[restKey], { validate }, schema); mongoWhere[out.key] = out.value; } return mongoWhere; diff --git a/src/Auth.js b/src/Auth.js index bcee1783fe..f21bdc763f 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -52,11 +52,7 @@ var getAuthForSessionToken = function({ config, sessionToken, installationId } = limit: 1, include: 'user' }; - var restWhere = { - _session_token: sessionToken - }; - var query = new RestQuery(config, master(config), '_Session', - restWhere, restOptions); + var query = new RestQuery(config, master(config), '_Session', { sessionToken }, restOptions); return query.execute().then((response) => { var results = response.results; if (results.length !== 1 || !results[0]['user']) { diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index bda0b06601..de78895ed5 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -158,20 +158,15 @@ DatabaseController.prototype.update = function(className, query, update, { var isMaster = acl === undefined; var aclGroup = acl || []; - var mongoUpdate, schema; + var mongoUpdate; return this.loadSchema() - .then(s => { - schema = s; - if (!isMaster) { - return schema.validatePermission(className, aclGroup, 'update'); - } - return Promise.resolve(); - }) + .then(schemaController => { + return (isMaster ? Promise.resolve() : schemaController.validatePermission(className, aclGroup, 'update')) .then(() => this.handleRelationUpdates(className, query.objectId, update)) .then(() => this.adapter.adaptiveCollection(className)) .then(collection => { if (!isMaster) { - query = this.addPointerPermissions(schema, className, 'update', query, aclGroup); + query = this.addPointerPermissions(schemaController, className, 'update', query, aclGroup); } if (!query) { return Promise.resolve(); @@ -179,26 +174,42 @@ DatabaseController.prototype.update = function(className, query, update, { if (acl) { query = addWriteACL(query, acl); } - var mongoWhere = this.transform.transformWhere(schema, className, query, {validate: !this.skipValidation}); - mongoUpdate = this.transform.transformUpdate(schema, 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); - } + 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 => { + 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 => { if (!result) { - return Promise.reject(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, - 'Object not found.')); + return Promise.reject(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.')); } if (this.skipValidation) { return Promise.resolve(result); } return sanitizeDatabaseResult(originalUpdate, result); }); + }); }; function sanitizeDatabaseResult(originalObject, result) { @@ -317,7 +328,16 @@ DatabaseController.prototype.destroy = function(className, query, { acl } = {}) if (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 => { // 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) { @@ -593,56 +613,59 @@ DatabaseController.prototype.find = function(className, query, { } let isMaster = acl === undefined; let aclGroup = acl || []; - let schema = null; - let op = typeof query.objectId == 'string' && Object.keys(query).length === 1 ? - 'get' : - 'find'; - return this.loadSchema().then(s => { - schema = s; + let op = typeof query.objectId == 'string' && Object.keys(query).length === 1 ? 'get' : 'find'; + return this.loadSchema() + .then(schemaController => { if (sort) { mongoOptions.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]; } } - - if (!isMaster) { - return schema.validatePermission(className, aclGroup, op); - } - return Promise.resolve(); - }) - .then(() => this.reduceRelationKeys(className, query)) - .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([]); + return (isMaster ? Promise.resolve() : schemaController.validatePermission(className, aclGroup, op)) + .then(() => this.reduceRelationKeys(className, query)) + .then(() => this.reduceInRelation(className, query, schemaController)) + .then(() => this.adapter.adaptiveCollection(className)) + .then(collection => { + if (!isMaster) { + query = this.addPointerPermissions(schemaController, className, op, query, aclGroup); } - } - if (!isMaster) { - query = addReadACL(query, aclGroup); - } - let mongoWhere = this.transform.transformWhere(schema, className, query); - 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( - schema, isMaster, aclGroup, className, r); + if (!query) { + if (op == 'get') { + return Promise.reject(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, + 'Object not found.')); + } else { + return Promise.resolve([]); + } + } + if (!isMaster) { + query = addReadACL(query, aclGroup); + } + 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 => { + 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); + }); }); - }); - } + } + }); + }); }); }; diff --git a/src/Routers/GlobalConfigRouter.js b/src/Routers/GlobalConfigRouter.js index c6a82dbbf4..ab49852273 100644 --- a/src/Routers/GlobalConfigRouter.js +++ b/src/Routers/GlobalConfigRouter.js @@ -1,12 +1,12 @@ // global_config.js -import PromiseRouter from '../PromiseRouter'; +import PromiseRouter from '../PromiseRouter'; import * as middleware from "../middlewares"; export class GlobalConfigRouter extends PromiseRouter { getGlobalConfig(req) { 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 there is no config in the database - return empty config. return { response: { params: {} } }; diff --git a/src/Routers/SessionsRouter.js b/src/Routers/SessionsRouter.js index 1a8d8cbfb9..1bae334464 100644 --- a/src/Routers/SessionsRouter.js +++ b/src/Routers/SessionsRouter.js @@ -1,8 +1,8 @@ import ClassesRouter from './ClassesRouter'; import PromiseRouter from '../PromiseRouter'; -import rest from '../rest'; -import Auth from '../Auth'; +import rest from '../rest'; +import Auth from '../Auth'; export class SessionsRouter extends ClassesRouter { handleFind(req) { @@ -36,7 +36,7 @@ export class SessionsRouter extends ClassesRouter { throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, '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) => { if (!response.results || response.results.length == 0) { throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index 4d4cb5034c..adba752f83 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -46,7 +46,7 @@ export class UsersRouter extends ClassesRouter { } let sessionToken = req.info.sessionToken; return rest.find(req.config, Auth.master(req.config), '_Session', - { _session_token: sessionToken }, + { sessionToken }, { include: 'user' }) .then((response) => { if (!response.results || @@ -139,7 +139,7 @@ export class UsersRouter extends ClassesRouter { let success = {response: {}}; if (req.info && req.info.sessionToken) { return rest.find(req.config, Auth.master(req.config), '_Session', - { _session_token: req.info.sessionToken } + { sessionToken: req.info.sessionToken } ).then((records) => { if (records.results && records.results.length) { return rest.del(req.config, Auth.master(req.config), '_Session',