From 21e73ac276d1d2fbd5642d98b56e55016e780a87 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 25 Apr 2016 17:21:24 -0700 Subject: [PATCH 01/48] Destructuring --- src/Adapters/Storage/Mongo/MongoTransform.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 34f0f15779..87ef89b0e0 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -187,15 +187,13 @@ export function transformKeyValue(schema, className, restKey, restValue, { // 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(schema, className, restWhere, { validate = true } = {}) { 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 = transformKeyValue(schema, className, restKey, restWhere[restKey], { query: true, validate }); mongoWhere[out.key] = out.value; } return mongoWhere; From e0a9f198952987f023e3ade718eeb1c59bd38a26 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 25 Apr 2016 18:22:20 -0700 Subject: [PATCH 02/48] Split logic for transforming queries into it's own function --- src/Adapters/Storage/Mongo/MongoTransform.js | 171 ++++++++++++++----- 1 file changed, 126 insertions(+), 45 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 87ef89b0e0..95c5fee5ee 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,26 +62,128 @@ 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'); + throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'you can only use $or in queries'); + case '$and': + 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) { + 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); } + } + + // Handle special schema key changes + // TODO: it seems like this is likely to have edge cases where + // pointer types are missed + var expected = undefined; + if (schema && schema.getExpectedType) { + expected = schema.getExpectedType(className, key); + } + if ((expected && expected.type == 'Pointer') || + (!expected && restValue && restValue.__type == 'Pointer')) { + key = '_p_' + key; + } + var expectedTypeIsArray = (expected && expected.type === 'Array'); + + // Handle atomic values + var value = transformAtom(restValue, false, { inArray, inObject }); + if (value !== CannotTransform) { + if (timeField && (typeof value === 'string')) { + value = new Date(value); + } + return {key: key, value: value}; + } + + // ACLs are handled before this method is called + // If an ACL key still exists here, something is wrong. + if (key === 'ACL') { + throw 'There was a problem transforming an ACL.'; + } + + // Handle arrays + if (restValue instanceof Array) { + value = restValue.map((restObj) => { + var out = transformKeyValue(schema, className, restKey, restObj, { inArray: true }); + return out.value; + }); + return {key: key, value: value}; + } + + // Handle update operators + value = transformUpdateOperator(restValue, !update); + if (value !== CannotTransform) { + return {key: key, value: value}; + } + + // Handle normal objects by recursing + value = {}; + for (var subRestKey in restValue) { + var subRestValue = restValue[subRestKey]; + var out = transformKeyValue(schema, className, subRestKey, subRestValue, { inObject: true }); + // For recursed objects, keep the keys in rest format + value[subRestKey] = out.value; + } + return {key: key, value: value}; +} + +function transformQueryKeyValue(schema, className, restKey, restValue, { + inArray, + inObject, + update, + validate, +} = {}) { + // Check if the schema is known since it's a built-in field. + var key = restKey; + var timeField = false; + switch(key) { + case 'objectId': + case '_id': + key = '_id'; + break; + case 'createdAt': + case '_created_at': + key = '_created_at'; + timeField = true; + break; + case 'updatedAt': + case '_updated_at': + key = '_updated_at'; + timeField = true; + break; + case '_email_verify_token': + key = "_email_verify_token"; + break; + case '_perishable_token': + key = "_perishable_token"; + break; + case 'sessionToken': + case '_session_token': + key = '_session_token'; + break; + case 'expiresAt': + case '_expiresAt': + key = 'expiresAt'; + timeField = true; + break; + case '_rperm': + case '_wperm': + return {key: key, value: restValue}; + break; + case '$or': if (!(restValue instanceof Array)) { - throw new Parse.Error(Parse.Error.INVALID_QUERY, - 'bad $or format - use an array value'); + 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': - 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'); + 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); @@ -94,19 +192,13 @@ export function transformKeyValue(schema, className, restKey, restValue, { 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; - }; + if (authDataMatch ) { + var provider = authDataMatch[1]; + // Special-case auth data. + return {key: '_auth_data_'+provider+'.id', value: restValue}; + } 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); } } @@ -124,14 +216,12 @@ 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}; - } + value = transformConstraint(restValue, expectedTypeIsArray); + if (value !== CannotTransform) { + return {key: key, value: value}; } - if (expectedTypeIsArray && query && !(restValue instanceof Array)) { + if (expectedTypeIsArray && !(restValue instanceof Array)) { return { key: key, value: { '$all' : [restValue] } }; @@ -154,15 +244,7 @@ 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; - }); - return {key: key, value: value}; + throw new Parse.Error(Parse.Error.INVALID_JSON,'cannot use array as query param'); } // Handle update operators @@ -182,7 +264,6 @@ export function transformKeyValue(schema, className, restKey, restValue, { return {key: key, value: value}; } - // Main exposed method to help run queries. // restWhere is the "where" clause in REST API form. // Returns the mongo form of the query. @@ -193,7 +274,7 @@ function transformWhere(schema, className, restWhere, { validate = true } = {}) throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.'); } for (let restKey in restWhere) { - let out = transformKeyValue(schema, className, restKey, restWhere[restKey], { query: true, validate }); + let out = transformQueryKeyValue(schema, className, restKey, restWhere[restKey], { validate }); mongoWhere[out.key] = out.value; } return mongoWhere; From 5c7b346292e5661bd06d2f72584d3b05581cc6f5 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 25 Apr 2016 18:48:22 -0700 Subject: [PATCH 03/48] Remove unnecessary logic --- src/Adapters/Storage/Mongo/MongoTransform.js | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 95c5fee5ee..7526c9cbbb 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -130,12 +130,7 @@ function transformKeyValue(schema, className, restKey, restValue, { return {key: key, value: value}; } -function transformQueryKeyValue(schema, className, restKey, restValue, { - inArray, - inObject, - update, - validate, -} = {}) { +function transformQueryKeyValue(schema, className, restKey, restValue, { validate } = {}) { // Check if the schema is known since it's a built-in field. var key = restKey; var timeField = false; @@ -228,7 +223,7 @@ function transformQueryKeyValue(schema, className, restKey, restValue, { } // Handle atomic values - var value = transformAtom(restValue, false, { inArray, inObject }); + var value = transformAtom(restValue, false); if (value !== CannotTransform) { if (timeField && (typeof value === 'string')) { value = new Date(value); @@ -236,19 +231,13 @@ function transformQueryKeyValue(schema, className, restKey, restValue, { return {key: key, value: value}; } - // ACLs are handled before this method is called - // If an ACL key still exists here, something is wrong. - if (key === 'ACL') { - throw 'There was a problem transforming an ACL.'; - } - // Handle arrays if (restValue instanceof Array) { throw new Parse.Error(Parse.Error.INVALID_JSON,'cannot use array as query param'); } // Handle update operators - value = transformUpdateOperator(restValue, !update); + value = transformUpdateOperator(restValue, true); if (value !== CannotTransform) { return {key: key, value: value}; } From e2a321335157a7052f9eb663e0b026634509addd Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 25 Apr 2016 19:34:07 -0700 Subject: [PATCH 04/48] Remove update logic from query --- src/Adapters/Storage/Mongo/MongoTransform.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 7526c9cbbb..783ff98ec8 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -236,12 +236,6 @@ function transformQueryKeyValue(schema, className, restKey, restValue, { validat throw new Parse.Error(Parse.Error.INVALID_JSON,'cannot use array as query param'); } - // Handle update operators - value = transformUpdateOperator(restValue, true); - if (value !== CannotTransform) { - return {key: key, value: value}; - } - // Handle normal objects by recursing value = {}; for (var subRestKey in restValue) { From ee8a3c94d51643f6957cc79e6a207402d6d3f661 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 25 Apr 2016 20:42:19 -0700 Subject: [PATCH 05/48] No need to transform post-transform keys in mongo adapter --- spec/ParseHooks.spec.js | 2 +- src/Adapters/Storage/Mongo/MongoTransform.js | 98 +++++++++----------- src/Auth.js | 6 +- src/Routers/GlobalConfigRouter.js | 4 +- src/Routers/SessionsRouter.js | 6 +- src/Routers/UsersRouter.js | 4 +- 6 files changed, 51 insertions(+), 69 deletions(-) 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/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 783ff98ec8..c537a1c4e4 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -130,59 +130,52 @@ function transformKeyValue(schema, className, restKey, restValue, { return {key: key, value: value}; } -function transformQueryKeyValue(schema, className, restKey, restValue, { validate } = {}) { +const valueAsDate = value => { + if (typeof value === 'string') { + return new Date(value); + } else if (value instanceof Date) { + return value; + } + return false; +} + +function transformQueryKeyValue(schema, className, key, value, { validate } = {}) { // Check if the schema is known since it's a built-in field. - var key = restKey; - var timeField = false; switch(key) { - case 'objectId': - case '_id': - key = '_id'; - break; case 'createdAt': - case '_created_at': + if (valueAsDate(value)) { + return {key: '_created_at', value: valueAsDate(value)} + } key = '_created_at'; - timeField = true; break; case 'updatedAt': - case '_updated_at': + if (valueAsDate(value)) { + return {key: '_updated_at', value: valueAsDate(value)} + } key = '_updated_at'; - timeField = true; - break; - case '_email_verify_token': - key = "_email_verify_token"; - break; - case '_perishable_token': - key = "_perishable_token"; - break; - case 'sessionToken': - case '_session_token': - key = '_session_token'; break; case 'expiresAt': - case '_expiresAt': - key = 'expiresAt'; - timeField = true; + 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': - return {key: key, value: restValue}; - break; + case '_perishable_token': + case '_email_verify_token': return {key, value} case '$or': - if (!(restValue instanceof Array)) { + if (!(value 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); - }); + var mongoSubqueries = value.map(subQuery => transformWhere(schema, className, subQuery)); return {key: '$or', value: mongoSubqueries}; case '$and': - if (!(restValue instanceof Array)) { + if (!(value 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); - }); + var mongoSubqueries = value.map(subQuery => transformWhere(schema, className, subQuery)); return {key: '$and', value: mongoSubqueries}; default: // Other auth data @@ -190,7 +183,7 @@ function transformQueryKeyValue(schema, className, restKey, restValue, { validat if (authDataMatch ) { var provider = authDataMatch[1]; // Special-case auth data. - return {key: '_auth_data_'+provider+'.id', value: restValue}; + 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); @@ -205,46 +198,39 @@ function transformQueryKeyValue(schema, className, restKey, restValue, { validat expected = schema.getExpectedType(className, key); } if ((expected && expected.type == 'Pointer') || - (!expected && restValue && restValue.__type == 'Pointer')) { + (!expected && value && value.__type == 'Pointer')) { key = '_p_' + key; } var expectedTypeIsArray = (expected && expected.type === 'Array'); // Handle query constraints - value = transformConstraint(restValue, expectedTypeIsArray); - if (value !== CannotTransform) { - return {key: key, value: value}; + if (transformConstraint(value, expectedTypeIsArray) !== CannotTransform) { + return {key, value: transformConstraint(value, expectedTypeIsArray)}; } - if (expectedTypeIsArray && !(restValue instanceof Array)) { - return { - key: key, value: { '$all' : [restValue] } - }; + if (expectedTypeIsArray && !(value instanceof Array)) { + return {key, value: { '$all' : [value] }}; } // Handle atomic values - var value = transformAtom(restValue, false); - if (value !== CannotTransform) { - if (timeField && (typeof value === 'string')) { - value = new Date(value); - } - return {key: key, value: value}; + if (transformAtom(value, false) !== CannotTransform) { + return {key, value: transformAtom(value, false)}; } // Handle arrays - if (restValue instanceof Array) { - throw new Parse.Error(Parse.Error.INVALID_JSON,'cannot use array as query param'); + if (value instanceof Array) { + throw new Parse.Error(Parse.Error.INVALID_JSON, 'cannot use array as query param'); } // Handle normal objects by recursing - value = {}; - for (var subRestKey in restValue) { - var subRestValue = restValue[subRestKey]; + let result = {}; + for (var subRestKey in value) { + var subRestValue = value[subRestKey]; var out = transformKeyValue(schema, className, subRestKey, subRestValue, { inObject: true }); // For recursed objects, keep the keys in rest format - value[subRestKey] = out.value; + result[subRestKey] = out.value; } - return {key: key, value: value}; + return {key, result}; } // Main exposed method to help run queries. 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/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', From 7b431ad0647a3163bae64a7d0cd685660efc3ed1 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 25 Apr 2016 20:54:47 -0700 Subject: [PATCH 06/48] nits --- src/Adapters/Storage/Mongo/MongoTransform.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index c537a1c4e4..5056c7a414 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -180,7 +180,7 @@ function transformQueryKeyValue(schema, className, key, value, { validate } = {} default: // Other auth data var authDataMatch = key.match(/^authData\.([a-zA-Z0-9_]+)\.id$/); - if (authDataMatch ) { + if (authDataMatch) { var provider = authDataMatch[1]; // Special-case auth data. return {key: `_auth_data_${provider}.id`, value}; From a3179e43fbe3d7559cdaf1b9d6824e7fc56dd7d8 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 25 Apr 2016 21:13:34 -0700 Subject: [PATCH 07/48] No need to handle object or reject arrays when transforming queries --- src/Adapters/Storage/Mongo/MongoTransform.js | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 5056c7a414..67c3282c33 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -215,22 +215,9 @@ function transformQueryKeyValue(schema, className, key, value, { validate } = {} // 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.`); } - - // Handle arrays - if (value instanceof Array) { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'cannot use array as query param'); - } - - // Handle normal objects by recursing - let result = {}; - for (var subRestKey in value) { - var subRestValue = value[subRestKey]; - var out = transformKeyValue(schema, className, subRestKey, subRestValue, { inObject: true }); - // For recursed objects, keep the keys in rest format - result[subRestKey] = out.value; - } - return {key, result}; } // Main exposed method to help run queries. From 37953d146b5243288cf2fb7a1bb396baf63b3b5a Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 25 Apr 2016 21:53:57 -0700 Subject: [PATCH 08/48] some cleanup --- src/Adapters/Storage/Mongo/MongoTransform.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 67c3282c33..bd7fb8951d 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -169,19 +169,17 @@ function transformQueryKeyValue(schema, className, key, value, { validate } = {} if (!(value instanceof Array)) { throw new Parse.Error(Parse.Error.INVALID_QUERY, 'bad $or format - use an array value'); } - var mongoSubqueries = value.map(subQuery => transformWhere(schema, className, subQuery)); - return {key: '$or', value: mongoSubqueries}; + return {key: '$or', value: value.map(subQuery => transformWhere(schema, className, subQuery))}; case '$and': if (!(value instanceof Array)) { throw new Parse.Error(Parse.Error.INVALID_QUERY, 'bad $and format - use an array value'); } - var mongoSubqueries = value.map(subQuery => transformWhere(schema, className, subQuery)); - return {key: '$and', value: mongoSubqueries}; + return {key: '$and', value: value.map(subQuery => transformWhere(schema, className, subQuery))}; default: // Other auth data - var authDataMatch = key.match(/^authData\.([a-zA-Z0-9_]+)\.id$/); + const authDataMatch = key.match(/^authData\.([a-zA-Z0-9_]+)\.id$/); if (authDataMatch) { - var provider = authDataMatch[1]; + const provider = authDataMatch[1]; // Special-case auth data. return {key: `_auth_data_${provider}.id`, value}; } @@ -193,7 +191,7 @@ function transformQueryKeyValue(schema, className, key, value, { validate } = {} // Handle special schema key changes // TODO: it seems like this is likely to have edge cases where // pointer types are missed - var expected = undefined; + let expected = undefined; if (schema && schema.getExpectedType) { expected = schema.getExpectedType(className, key); } @@ -201,7 +199,7 @@ function transformQueryKeyValue(schema, className, key, value, { validate } = {} (!expected && value && value.__type == 'Pointer')) { key = '_p_' + key; } - var expectedTypeIsArray = (expected && expected.type === 'Array'); + const expectedTypeIsArray = (expected && expected.type === 'Array'); // Handle query constraints if (transformConstraint(value, expectedTypeIsArray) !== CannotTransform) { From 5cbf3eb8dd8e712829e2277453fe5c7b4ed74e34 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 25 Apr 2016 22:01:50 -0700 Subject: [PATCH 09/48] Tidy up db controller --- src/Adapters/Storage/Mongo/MongoTransform.js | 4 --- src/Controllers/DatabaseController.js | 31 ++++++++++++-------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index bd7fb8951d..5c724c579b 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -140,7 +140,6 @@ const valueAsDate = value => { } function transformQueryKeyValue(schema, className, key, value, { validate } = {}) { - // Check if the schema is known since it's a built-in field. switch(key) { case 'createdAt': if (valueAsDate(value)) { @@ -188,9 +187,6 @@ function transformQueryKeyValue(schema, className, key, value, { validate } = {} } } - // Handle special schema key changes - // TODO: it seems like this is likely to have edge cases where - // pointer types are missed let expected = undefined; if (schema && schema.getExpectedType) { expected = schema.getExpectedType(className, key); diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index fca0282967..86dd737aab 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,8 +174,18 @@ 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}); + var mongoWhere = this.transform.transformWhere( + schemaController, + className, + query, + {validate: !this.skipValidation} + ); + mongoUpdate = this.transform.transformUpdate( + schemaController, + className, + update, + {validate: !this.skipValidation} + ); if (many) { return collection.updateMany(mongoWhere, mongoUpdate); } else if (upsert) { @@ -191,14 +196,14 @@ DatabaseController.prototype.update = function(className, query, update, { }) .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) { From 608cba9e8cf6a38d5ea4655b73e6d55f8e9adccb Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 25 Apr 2016 22:12:03 -0700 Subject: [PATCH 10/48] Clearer names in DatabaseController --- src/Controllers/DatabaseController.js | 70 ++++++++++++--------------- 1 file changed, 31 insertions(+), 39 deletions(-) diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 86dd737aab..907da41ecc 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -598,56 +598,48 @@ 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) + 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); + } + let mongoWhere = this.transform.transformWhere(schemaController, 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); + return this.untransformObject(schemaController, isMaster, aclGroup, className, r); }); }); - } + } + }); }); }; From 4371ca164cb4dceee36af4c7142a4a191bf63ad7 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 25 Apr 2016 22:45:16 -0700 Subject: [PATCH 11/48] Pass parse format schema to transformWhere --- .../Storage/Mongo/MongoStorageAdapter.js | 5 +- src/Adapters/Storage/Mongo/MongoTransform.js | 10 +- src/Controllers/DatabaseController.js | 100 ++++++++++++------ 3 files changed, 77 insertions(+), 38 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 803b3c63b7..a6a642da8a 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -174,14 +174,15 @@ export class MongoStorageAdapter { // 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) { + deleteObjectsByQuery(className, query, schemaController, validate, parseFormatSchema) { return this.adaptiveCollection(className) .then(collection => { let mongoWhere = transform.transformWhere( schemaController, className, query, - { validate } + { validate }, + parseFormatSchema ); return collection.deleteMany(mongoWhere) }) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 5c724c579b..ac056a2074 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -139,7 +139,7 @@ const valueAsDate = value => { return false; } -function transformQueryKeyValue(schema, className, key, value, { validate } = {}) { +function transformQueryKeyValue(schema, className, key, value, { validate } = {}, parseFormatSchema) { switch(key) { case 'createdAt': if (valueAsDate(value)) { @@ -168,12 +168,12 @@ function transformQueryKeyValue(schema, className, key, value, { validate } = {} 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(schema, className, subQuery))}; + return {key: '$or', value: value.map(subQuery => transformWhere(schema, className, subQuery, parseFormatSchema))}; 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(schema, className, subQuery))}; + return {key: '$and', value: value.map(subQuery => transformWhere(schema, className, subQuery, parseFormatSchema))}; default: // Other auth data const authDataMatch = key.match(/^authData\.([a-zA-Z0-9_]+)\.id$/); @@ -218,13 +218,13 @@ function transformQueryKeyValue(schema, className, key, value, { validate } = {} // 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, { validate = true } = {}) { +function transformWhere(schema, className, restWhere, { validate = true } = {}, parseFormatSchema) { let mongoWhere = {}; if (restWhere['ACL']) { throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.'); } for (let restKey in restWhere) { - let out = transformQueryKeyValue(schema, className, restKey, restWhere[restKey], { validate }); + let out = transformQueryKeyValue(schema, className, restKey, restWhere[restKey], { validate }, parseFormatSchema); mongoWhere[out.key] = out.value; } return mongoWhere; diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 907da41ecc..b6f6b9a68a 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -174,25 +174,37 @@ DatabaseController.prototype.update = function(className, query, update, { if (acl) { query = addWriteACL(query, acl); } - var mongoWhere = this.transform.transformWhere( - schemaController, - className, - query, - {validate: !this.skipValidation} - ); - 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); - } + 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( + schemaController, + 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) { @@ -322,7 +334,22 @@ 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, + schemaController, + !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) { @@ -627,18 +654,29 @@ DatabaseController.prototype.find = function(className, query, { if (!isMaster) { query = addReadACL(query, aclGroup); } - let mongoWhere = this.transform.transformWhere(schemaController, 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(schemaController, isMaster, aclGroup, className, r); + 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(schemaController, 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); + }); }); - }); - } + } + }); }); }); }; From 874d10fc746d73915de3d3509185bc3fc7c8af6f Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 25 Apr 2016 23:36:25 -0700 Subject: [PATCH 12/48] Check expected type is array from Parse Format Schema --- src/Adapters/Storage/Mongo/MongoTransform.js | 5 ++++- src/Controllers/DatabaseController.js | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index ac056a2074..307783abbc 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -195,7 +195,10 @@ function transformQueryKeyValue(schema, className, key, value, { validate } = {} (!expected && value && value.__type == 'Pointer')) { key = '_p_' + key; } - const expectedTypeIsArray = (expected && expected.type === 'Array'); + const expectedTypeIsArray = + parseFormatSchema && + parseFormatSchema.fields[key] && + parseFormatSchema.fields[key].type === 'Array' // Handle query constraints if (transformConstraint(value, expectedTypeIsArray) !== CannotTransform) { diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index b6f6b9a68a..cb40d10144 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -654,7 +654,8 @@ DatabaseController.prototype.find = function(className, query, { if (!isMaster) { query = addReadACL(query, aclGroup); } - return schemaController.getOneSchema(className) + return schemaController.reloadData() + .then(() => schemaController.getOneSchema(className)) .catch(error => { // If the schema doesn't exist, pretend it exists with no fields. This behaviour // will likely need revisiting. @@ -664,7 +665,7 @@ DatabaseController.prototype.find = function(className, query, { throw error; }) .then(parseFormatSchema => { - let mongoWhere = this.transform.transformWhere(schemaController, className, query, parseFormatSchema); + let mongoWhere = this.transform.transformWhere(schemaController, className, query, {}, parseFormatSchema); if (count) { delete mongoOptions.limit; return collection.count(mongoWhere, mongoOptions); From 5f564f32f5e5f9d2e114ac7ed7bd5c187d957771 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 25 Apr 2016 23:39:42 -0700 Subject: [PATCH 13/48] simplify a little --- src/Controllers/DatabaseController.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index cb40d10144..74ffd443ee 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -654,8 +654,7 @@ DatabaseController.prototype.find = function(className, query, { if (!isMaster) { query = addReadACL(query, aclGroup); } - return schemaController.reloadData() - .then(() => schemaController.getOneSchema(className)) + return schemaController.getOneSchema(className) .catch(error => { // If the schema doesn't exist, pretend it exists with no fields. This behaviour // will likely need revisiting. From a926712951db54984cd15fd8a252b7f5c042fd26 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Tue, 26 Apr 2016 10:12:45 -0700 Subject: [PATCH 14/48] Start using parse format schema in transformQueryKeyValue --- spec/Schema.spec.js | 1 - src/Adapters/Storage/Mongo/MongoTransform.js | 23 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) 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/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 307783abbc..0fe8e40e2a 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -168,12 +168,12 @@ function transformQueryKeyValue(schema, className, key, value, { validate } = {} 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(schema, className, subQuery, parseFormatSchema))}; + return {key: '$or', value: value.map(subQuery => transformWhere(schema, className, subQuery, {}, parseFormatSchema))}; 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(schema, className, subQuery, parseFormatSchema))}; + return {key: '$and', value: value.map(subQuery => transformWhere(schema, className, subQuery, {}, parseFormatSchema))}; default: // Other auth data const authDataMatch = key.match(/^authData\.([a-zA-Z0-9_]+)\.id$/); @@ -187,18 +187,19 @@ function transformQueryKeyValue(schema, className, key, value, { validate } = {} } } - let expected = undefined; - if (schema && schema.getExpectedType) { - expected = schema.getExpectedType(className, key); - } - if ((expected && expected.type == 'Pointer') || - (!expected && value && value.__type == 'Pointer')) { - key = '_p_' + key; - } const expectedTypeIsArray = parseFormatSchema && parseFormatSchema.fields[key] && - parseFormatSchema.fields[key].type === 'Array' + parseFormatSchema.fields[key].type === 'Array'; + + const expectedTypeIsPointer = + parseFormatSchema && + parseFormatSchema.fields[key] && + parseFormatSchema.fields[key].type === 'Pointer'; + + if (expectedTypeIsPointer || !parseFormatSchema && value && value.__type === 'Pointer') { + key = '_p_' + key; + } // Handle query constraints if (transformConstraint(value, expectedTypeIsArray) !== CannotTransform) { From d4bd21fcbc8e5513861a4023d5e903b4c15c843e Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Tue, 26 Apr 2016 10:20:17 -0700 Subject: [PATCH 15/48] remove schema from transformWhere --- spec/MongoTransform.spec.js | 4 ++-- src/Adapters/Storage/Mongo/MongoStorageAdapter.js | 8 +------- src/Adapters/Storage/Mongo/MongoTransform.js | 10 +++++----- src/Controllers/DatabaseController.js | 10 ++-------- 4 files changed, 10 insertions(+), 22 deletions(-) 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/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index a6a642da8a..091129a849 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -177,13 +177,7 @@ export class MongoStorageAdapter { deleteObjectsByQuery(className, query, schemaController, validate, parseFormatSchema) { return this.adaptiveCollection(className) .then(collection => { - let mongoWhere = transform.transformWhere( - schemaController, - className, - query, - { validate }, - parseFormatSchema - ); + let mongoWhere = transform.transformWhere(className, query, { validate }, parseFormatSchema); return collection.deleteMany(mongoWhere) }) .then(({ result }) => { diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 0fe8e40e2a..ba9d399222 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -139,7 +139,7 @@ const valueAsDate = value => { return false; } -function transformQueryKeyValue(schema, className, key, value, { validate } = {}, parseFormatSchema) { +function transformQueryKeyValue(className, key, value, { validate } = {}, parseFormatSchema) { switch(key) { case 'createdAt': if (valueAsDate(value)) { @@ -168,12 +168,12 @@ function transformQueryKeyValue(schema, className, key, value, { validate } = {} 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(schema, className, subQuery, {}, parseFormatSchema))}; + return {key: '$or', value: value.map(subQuery => transformWhere(className, subQuery, {}, parseFormatSchema))}; 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(schema, className, subQuery, {}, parseFormatSchema))}; + return {key: '$and', value: value.map(subQuery => transformWhere(className, subQuery, {}, parseFormatSchema))}; default: // Other auth data const authDataMatch = key.match(/^authData\.([a-zA-Z0-9_]+)\.id$/); @@ -222,13 +222,13 @@ function transformQueryKeyValue(schema, className, key, value, { validate } = {} // 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, { validate = true } = {}, parseFormatSchema) { +function transformWhere(className, restWhere, { validate = true } = {}, parseFormatSchema) { let mongoWhere = {}; if (restWhere['ACL']) { throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.'); } for (let restKey in restWhere) { - let out = transformQueryKeyValue(schema, className, restKey, restWhere[restKey], { validate }, parseFormatSchema); + let out = transformQueryKeyValue(className, restKey, restWhere[restKey], { validate }, parseFormatSchema); mongoWhere[out.key] = out.value; } return mongoWhere; diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 74ffd443ee..9ec262366a 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -184,13 +184,7 @@ DatabaseController.prototype.update = function(className, query, update, { throw error; }) .then(parseFormatSchema => { - var mongoWhere = this.transform.transformWhere( - schemaController, - className, - query, - {validate: !this.skipValidation}, - parseFormatSchema - ); + var mongoWhere = this.transform.transformWhere(className, query, {validate: !this.skipValidation}, parseFormatSchema); mongoUpdate = this.transform.transformUpdate( schemaController, className, @@ -664,7 +658,7 @@ DatabaseController.prototype.find = function(className, query, { throw error; }) .then(parseFormatSchema => { - let mongoWhere = this.transform.transformWhere(schemaController, className, query, {}, parseFormatSchema); + let mongoWhere = this.transform.transformWhere(className, query, {}, parseFormatSchema); if (count) { delete mongoOptions.limit; return collection.count(mongoWhere, mongoOptions); From 449ca115b13c5a1f7f24a375faa2354c07fdf7c0 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Tue, 26 Apr 2016 10:23:14 -0700 Subject: [PATCH 16/48] Break object deletions's dependency on schemaController --- src/Adapters/Storage/Mongo/MongoStorageAdapter.js | 6 +++--- src/Controllers/DatabaseController.js | 8 +------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 091129a849..f6467386de 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -173,11 +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, parseFormatSchema) { + // Currently accepts the validate for lecacy 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(className, query, { validate }, parseFormatSchema); + let mongoWhere = transform.transformWhere(className, query, { validate }, schema); return collection.deleteMany(mongoWhere) }) .then(({ result }) => { diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 9ec262366a..6897766e61 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -337,13 +337,7 @@ DatabaseController.prototype.destroy = function(className, query, { acl } = {}) } throw error; }) - .then(parseFormatSchema => this.adapter.deleteObjectsByQuery( - className, - query, - schemaController, - !this.skipValidation, - parseFormatSchema - )) + .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) { From 71ae7bee945c7a341b3935e6b7fce14bfa058438 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Tue, 26 Apr 2016 13:08:58 -0700 Subject: [PATCH 17/48] better names and comments --- .../Storage/Mongo/MongoStorageAdapter.js | 2 +- src/Adapters/Storage/Mongo/MongoTransform.js | 24 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index f6467386de..d61df40cbc 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -173,7 +173,7 @@ 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 validate for lecacy reasons. Currently accepts the schema, that may not actually be necessary. + // 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 => { diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index ba9d399222..0013a39d0e 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -139,7 +139,7 @@ const valueAsDate = value => { return false; } -function transformQueryKeyValue(className, key, value, { validate } = {}, parseFormatSchema) { +function transformQueryKeyValue(className, key, value, { validate } = {}, schema) { switch(key) { case 'createdAt': if (valueAsDate(value)) { @@ -168,12 +168,12 @@ function transformQueryKeyValue(className, key, value, { validate } = {}, parseF 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, {}, parseFormatSchema))}; + 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, {}, parseFormatSchema))}; + 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$/); @@ -188,16 +188,16 @@ function transformQueryKeyValue(className, key, value, { validate } = {}, parseF } const expectedTypeIsArray = - parseFormatSchema && - parseFormatSchema.fields[key] && - parseFormatSchema.fields[key].type === 'Array'; + schema && + schema.fields[key] && + schema.fields[key].type === 'Array'; const expectedTypeIsPointer = - parseFormatSchema && - parseFormatSchema.fields[key] && - parseFormatSchema.fields[key].type === 'Pointer'; + schema && + schema.fields[key] && + schema.fields[key].type === 'Pointer'; - if (expectedTypeIsPointer || !parseFormatSchema && value && value.__type === 'Pointer') { + if (expectedTypeIsPointer || !schema && value && value.__type === 'Pointer') { key = '_p_' + key; } @@ -222,13 +222,13 @@ function transformQueryKeyValue(className, key, value, { validate } = {}, parseF // 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(className, restWhere, { validate = true } = {}, parseFormatSchema) { +function transformWhere(className, restWhere, { validate = true } = {}, schema) { let mongoWhere = {}; if (restWhere['ACL']) { throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.'); } for (let restKey in restWhere) { - let out = transformQueryKeyValue(className, restKey, restWhere[restKey], { validate }, parseFormatSchema); + let out = transformQueryKeyValue(className, restKey, restWhere[restKey], { validate }, schema); mongoWhere[out.key] = out.value; } return mongoWhere; From 90df7841ef7808aa5e595e6c70442bffea4d1742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ramos?= Date: Tue, 3 May 2016 12:08:23 -0700 Subject: [PATCH 18/48] Update issue template Additional guidance for what type of details should be included with a bug report. --- .github/ISSUE_TEMPLATE.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 6e17e9548a..a20ffb4b6d 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,19 +1,28 @@ -Check out [this issue](https://github.com/ParsePlatform/parse-server/issues/1271) for an ideal bug report. The closer your issue report is to that one, the more likely we are to be able to help, and the more likely we will be to fix the issue quickly! +Check out https://github.com/ParsePlatform/parse-server/issues/1271 for an ideal bug report. The closer your issue report is to that one, the more likely we are to be able to help, and the more likely we will be to fix the issue quickly! -For implementation related questions or technical support, please refer to the [Stack Overflow](http://stackoverflow.com/questions/tagged/parse.com) and [Server Fault](https://serverfault.com/tags/parse) communities. For database migration help, please [file a bug report](https://parse.com/help#report). +Many members of the community use Stack Overflow and Server Fault to ask questions. Read through the existing questions or ask your own! + +- Stack Overflow: http://stackoverflow.com/questions/tagged/parse.com +- Server Fault: https://serverfault.com/tags/parse + +For database migration help, please file a bug report at https://parse.com/help#report Make sure these boxes are checked before submitting your issue -- thanks for reporting issues back to Parse Server! -- [ ] You've met the [prerequisites](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#prerequisites). +- [ ] You've met the prerequisites: https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#prerequisites. -- [ ] You're running the [latest version](https://github.com/ParsePlatform/parse-server/releases) of Parse Server. +- [ ] You're running the latest version of Parse Server: https://github.com/ParsePlatform/parse-server/releases -- [ ] You've searched through [existing issues](https://github.com/ParsePlatform/parse-server/issues?utf8=%E2%9C%93&q=). Chances are that your issue has been reported or resolved before. +- [ ] You've searched through existing issues: https://github.com/ParsePlatform/Parse-Server/issues?utf8=%E2%9C%93&q=is%3Aissue Chances are that your issue has been reported or resolved before. #### Environment Setup +- Server: parse-server version, operating system, hardware, local or remote? +- Database: version, storage engine, hardware, local or remote? #### Steps to reproduce +- Can this issue be reproduced using the Parse Server REST API? Include curl commands when applicable. +- What was the expected result? What is the actual outcome? #### Logs/Trace From 7c850fc1adb44825ac70e8cd5e19659d21bde4ec Mon Sep 17 00:00:00 2001 From: Steven Shipton Date: Fri, 6 May 2016 20:36:05 +0100 Subject: [PATCH 19/48] Fix for #1642 - copy query parameters to request body (#1687) * Fix for #1642 - copy query parameters to request body * Add missing request.query to pass test --- spec/InstallationsRouter.spec.js | 74 ++++++++++++++++++++++++++++++ src/Routers/InstallationsRouter.js | 22 +++++---- 2 files changed, 86 insertions(+), 10 deletions(-) create mode 100644 spec/InstallationsRouter.spec.js diff --git a/spec/InstallationsRouter.spec.js b/spec/InstallationsRouter.spec.js new file mode 100644 index 0000000000..924e799fd9 --- /dev/null +++ b/spec/InstallationsRouter.spec.js @@ -0,0 +1,74 @@ +var auth = require('../src/Auth'); +var Config = require('../src/Config'); +var rest = require('../src/rest'); +var InstallationsRouter = require('../src/Routers/InstallationsRouter').InstallationsRouter; + +var config = new Config('test'); + +describe('InstallationsRouter', () => { + it('uses find condition from request.body', (done) => { + var androidDeviceRequest = { + 'installationId': '12345678-abcd-abcd-abcd-123456789abc', + 'deviceType': 'android' + }; + var iosDeviceRequest = { + 'installationId': '12345678-abcd-abcd-abcd-123456789abd', + 'deviceType': 'ios' + }; + var request = { + config: config, + auth: auth.master(config), + body: { + where: { + deviceType: 'android' + } + }, + query: {} + }; + + var router = new InstallationsRouter(); + rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest) + .then(() => { + return rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest); + }).then(() => { + return router.handleFind(request); + }).then((res) => { + var results = res.response.results; + expect(results.length).toEqual(1); + done(); + }); + }); + + it('uses find condition from request.query', (done) => { + var androidDeviceRequest = { + 'installationId': '12345678-abcd-abcd-abcd-123456789abc', + 'deviceType': 'android' + }; + var iosDeviceRequest = { + 'installationId': '12345678-abcd-abcd-abcd-123456789abd', + 'deviceType': 'ios' + }; + var request = { + config: config, + auth: auth.master(config), + body: {}, + query: { + where: { + deviceType: 'android' + } + } + }; + + var router = new InstallationsRouter(); + rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest) + .then(() => { + return rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest); + }).then(() => { + return router.handleFind(request); + }).then((res) => { + var results = res.response.results; + expect(results.length).toEqual(1); + done(); + }); + }); +}); diff --git a/src/Routers/InstallationsRouter.js b/src/Routers/InstallationsRouter.js index 9c21f005bf..372bba8140 100644 --- a/src/Routers/InstallationsRouter.js +++ b/src/Routers/InstallationsRouter.js @@ -6,25 +6,27 @@ import rest from '../rest'; export class InstallationsRouter extends ClassesRouter { handleFind(req) { + let body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query)); var options = {}; - if (req.body.skip) { - options.skip = Number(req.body.skip); + + if (body.skip) { + options.skip = Number(body.skip); } - if (req.body.limit) { - options.limit = Number(req.body.limit); + if (body.limit) { + options.limit = Number(body.limit); } - if (req.body.order) { - options.order = String(req.body.order); + if (body.order) { + options.order = String(body.order); } - if (req.body.count) { + if (body.count) { options.count = true; } - if (req.body.include) { - options.include = String(req.body.include); + if (body.include) { + options.include = String(body.include); } return rest.find(req.config, req.auth, - '_Installation', req.body.where, options) + '_Installation', body.where, options) .then((response) => { return {response: response}; }); From da5b5ccefa369d3031e6d2cd0b2b12e9de038d35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ramos?= Date: Fri, 6 May 2016 12:45:47 -0700 Subject: [PATCH 20/48] Update ISSUE_TEMPLATE.md --- .github/ISSUE_TEMPLATE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index a20ffb4b6d..7c17eca414 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -26,3 +26,5 @@ Make sure these boxes are checked before submitting your issue -- thanks for rep - What was the expected result? What is the actual outcome? #### Logs/Trace + +- You can turn on additional logging by configuring VERBOSE=1 in your environment. From 37c502bed0dc458bc2f6fd4a85dfd854daff12ae Mon Sep 17 00:00:00 2001 From: Steven Shipton Date: Fri, 6 May 2016 20:50:45 +0100 Subject: [PATCH 21/48] Add config.expireInactiveSession to add support for non-expiring inactive sessions (#1536) * Create non-expiring session when sessionLength is zero * Introduce expireInactiveSessions setting --- spec/RestCreate.spec.js | 27 +++++++++++++++++++++++++++ spec/index.spec.js | 23 +++++++++++++++++++++++ src/Config.js | 22 ++++++++++++++-------- src/ParseServer.js | 2 ++ 4 files changed, 66 insertions(+), 8 deletions(-) diff --git a/spec/RestCreate.spec.js b/spec/RestCreate.spec.js index ffc19e4f26..13efc95f0a 100644 --- a/spec/RestCreate.spec.js +++ b/spec/RestCreate.spec.js @@ -352,4 +352,31 @@ describe('rest create', () => { done(); }); }); + + it("can create a session with no expiration", (done) => { + var user = { + username: 'asdf', + password: 'zxcv', + foo: 'bar' + }; + config.expireInactiveSessions = false; + + rest.create(config, auth.nobody(config), '_User', user) + .then((r) => { + expect(Object.keys(r.response).length).toEqual(3); + expect(typeof r.response.objectId).toEqual('string'); + expect(typeof r.response.createdAt).toEqual('string'); + expect(typeof r.response.sessionToken).toEqual('string'); + return rest.find(config, auth.master(config), + '_Session', {sessionToken: r.response.sessionToken}); + }) + .then((r) => { + expect(r.results.length).toEqual(1); + + var session = r.results[0]; + expect(session.expiresAt).toBeUndefined(); + + done(); + }); + }); }); diff --git a/spec/index.spec.js b/spec/index.spec.js index d2c8984d71..d0d54401b1 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -332,6 +332,29 @@ describe('server', () => { sessionLength: '0' })).toThrow('Session length must be a value greater than 0.'); done(); + }); + + it('ignores the session length when expireInactiveSessions set to false', (done) => { + expect(() => setServerConfiguration({ + serverURL: 'http://localhost:8378/1', + appId: 'test', + appName: 'unused', + javascriptKey: 'test', + masterKey: 'test', + sessionLength: '-33', + expireInactiveSessions: false + })).not.toThrow(); + + expect(() => setServerConfiguration({ + serverURL: 'http://localhost:8378/1', + appId: 'test', + appName: 'unused', + javascriptKey: 'test', + masterKey: 'test', + sessionLength: '0', + expireInactiveSessions: false + })).not.toThrow(); + done(); }) it('fails if you try to set revokeSessionOnPasswordReset to non-boolean', done => { diff --git a/src/Config.js b/src/Config.js index 5c3ca4e08e..badff9da40 100644 --- a/src/Config.js +++ b/src/Config.js @@ -48,6 +48,7 @@ export class Config { this.mount = removeTrailingSlash(mount); this.liveQueryController = cacheInfo.liveQueryController; this.sessionLength = cacheInfo.sessionLength; + this.expireInactiveSessions = cacheInfo.expireInactiveSessions; this.generateSessionExpiresAt = this.generateSessionExpiresAt.bind(this); this.revokeSessionOnPasswordReset = cacheInfo.revokeSessionOnPasswordReset; } @@ -69,7 +70,7 @@ export class Config { } } - this.validateSessionLength(options.sessionLength); + this.validateSessionConfiguration(options.sessionLength, options.expireInactiveSessions); } static validateEmailConfiguration({verifyUserEmails, appName, publicServerURL}) { @@ -95,16 +96,21 @@ export class Config { this._mount = newValue; } - static validateSessionLength(sessionLength) { - if(isNaN(sessionLength)) { - throw 'Session length must be a valid number.'; - } - else if(sessionLength <= 0) { - throw 'Session length must be a value greater than 0.' + static validateSessionConfiguration(sessionLength, expireInactiveSessions) { + if (expireInactiveSessions) { + if (isNaN(sessionLength)) { + throw 'Session length must be a valid number.'; + } + else if (sessionLength <= 0) { + throw 'Session length must be a value greater than 0.' + } } } generateSessionExpiresAt() { + if (!this.expireInactiveSessions) { + return undefined; + } var now = new Date(); return new Date(now.getTime() + (this.sessionLength*1000)); } @@ -132,7 +138,7 @@ export class Config { get verifyEmailURL() { return `${this.publicServerURL}/apps/${this.applicationId}/verify_email`; } -}; +} export default Config; module.exports = Config; diff --git a/src/ParseServer.js b/src/ParseServer.js index ad8efd19da..baeb1d69e5 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -114,6 +114,7 @@ class ParseServer { }, liveQuery = {}, sessionLength = 31536000, // 1 Year in seconds + expireInactiveSessions = true, verbose = false, revokeSessionOnPasswordReset = true, }) { @@ -188,6 +189,7 @@ class ParseServer { maxUploadSize: maxUploadSize, liveQueryController: liveQueryController, sessionLength: Number(sessionLength), + expireInactiveSessions: expireInactiveSessions, revokeSessionOnPasswordReset }); From 10d2988985c6936fe5027bf30ab1a2195756478a Mon Sep 17 00:00:00 2001 From: andrewchae Date: Mon, 9 May 2016 10:45:57 -0700 Subject: [PATCH 22/48] Pass dates to cloud code functions (#655) (#1697) --- src/Routers/FunctionsRouter.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index b405b90eb5..3b2c740a24 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -33,6 +33,14 @@ export class FunctionsRouter extends PromiseRouter { var theValidator = triggers.getValidator(req.params.functionName, applicationId); if (theFunction) { const params = Object.assign({}, req.body, req.query); + for (var key in params) { + if (params.hasOwnProperty(key)) { + var value = params[key]; + if (value.__type == 'Date') { + params[key] = new Date(value.iso); + } + } + } var request = { params: params, master: req.auth && req.auth.isMaster, From e60901de6a8cfa516138cd3c0adf09cc6b9c2b11 Mon Sep 17 00:00:00 2001 From: Drew Date: Mon, 9 May 2016 16:56:32 -0700 Subject: [PATCH 23/48] Version 2.2.8 (#1738) --- CHANGELOG.md | 17 +++++++++++++++++ package.json | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8812172a9..59f21ccd3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ ## Parse Server Changelog +### 2.2.8 (5/8/2016) + +* New: Support for Pointer Permissions +* New: Expose logger in Cloud Code +* New: Option to revoke sessions on password reset +* New: Option to expire inactive sessions +* Perf: Improvements in ACL checking query +* Fix: Issues when sending pushes to list of devices that contains invalid values +* Fix: Issues caused by using babel-polyfill outside of Parse Server, but in the same express app +* Fix: Remove creation of extra session tokens +* Fix: Return authData when querying with master key +* Fix: Bugs when deleting webhooks +* Fix: Ignore _RevocableSession header, which might be sent by the JS SDK +* Fix: Issues with querying via URL params +* Fix: Properly encode "Date" parameters to cloud code functions + + ### 2.2.7 (4/15/2016) * Adds support for --verbose and verbose option when running ParseServer [\#1414](https://github.com/ParsePlatform/parse-server/pull/1414) ([flovilmart](https://github.com/flovilmart)) diff --git a/package.json b/package.json index da3bb7a67a..503f8f66c6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "2.2.7", + "version": "2.2.8", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From e698f61a5826ea33055abf681aec7b09796adb01 Mon Sep 17 00:00:00 2001 From: Drew Date: Tue, 10 May 2016 10:43:02 -0700 Subject: [PATCH 24/48] Handle null param in cloud code. Fixes #1472 (#1746) --- spec/ParseAPI.spec.js | 16 ++++++++++++++++ src/Routers/FunctionsRouter.js | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index a1bb412f54..084b7fe337 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -1119,6 +1119,22 @@ describe('miscellaneous', function() { }); }); + it('can handle null params in cloud functions (regression test for #1742)', done => { + Parse.Cloud.define('func', (request, response) => { + expect(request.params.nullParam).toEqual(null); + response.success('yay'); + }); + + Parse.Cloud.run('func', {nullParam: null}) + .then(() => { + Parse.Cloud._removeHook('Functions', 'func'); + done() + }, e => { + fail('cloud code call failed'); + done(); + }); + }); + it('fails on invalid client key', done => { var headers = { 'Content-Type': 'application/octet-stream', diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index 3b2c740a24..ea614b4946 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -36,7 +36,7 @@ export class FunctionsRouter extends PromiseRouter { for (var key in params) { if (params.hasOwnProperty(key)) { var value = params[key]; - if (value.__type == 'Date') { + if (value && value.__type == 'Date') { params[key] = new Date(value.iso); } } From 26df50924b45dbbc20723e0fd43ce0de1329cb4f Mon Sep 17 00:00:00 2001 From: Drew Date: Tue, 10 May 2016 10:50:05 -0700 Subject: [PATCH 25/48] Update to 2.2.9 (#1747) --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59f21ccd3c..cecb204534 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Parse Server Changelog +### 2.2.9 (5/9/2016) + +* Fix: Fix a regression that caused Parse Server to crash when a null parameter is passed to a Cloud function + ### 2.2.8 (5/8/2016) * New: Support for Pointer Permissions diff --git a/package.json b/package.json index 503f8f66c6..14720edece 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "2.2.8", + "version": "2.2.9", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From 1291f70a16ac84a59be8d8cc82a9057b8485a960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Balay?= Date: Tue, 10 May 2016 19:13:06 -0300 Subject: [PATCH 26/48] Fix malformed config.json in bootstrap.sh The bootstrap bash script generates a wrong config.json file. Instead of having a databaseURI property, it has a mongodbURI prop. --- bootstrap.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bootstrap.sh b/bootstrap.sh index 25f9199bfa..4fb2fab3c1 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -103,7 +103,7 @@ cat > ./config.json << EOF "masterKey": "$MASTER_KEY", "appName": "$APP_NAME", "cloud": "./cloud/main", - "mongodbURI": "$MONGODB_URI" + "databaseURI": "$MONGODB_URI" } EOF echo "${CHECK} Created config.json" From 10ee229e606a7e5e14536dbc5675c280678f2968 Mon Sep 17 00:00:00 2001 From: Drew Date: Wed, 11 May 2016 05:54:23 -0700 Subject: [PATCH 27/48] Update dependencies and support node 6.1 (#1754) * Update dependencies and support node 6.1 * Remove gcs-adapter, which depends on grpc, which fails to build on node 6 * Remove GCS adapter usage --- .travis.yml | 1 + package.json | 8 +++----- spec/AdapterLoader.spec.js | 10 ---------- 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6c79fd16cf..68052a1f5b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: node_js node_js: - '4.3' +- '6.1' env: global: - COVERAGE_OPTION='./node_modules/babel-istanbul/lib/cli.js cover -x **/spec/**' diff --git a/package.json b/package.json index 14720edece..264f57ab6b 100644 --- a/package.json +++ b/package.json @@ -57,12 +57,10 @@ "codecov": "^1.0.1", "cross-env": "^1.0.7", "deep-diff": "^0.3.3", - "flow-bin": "^0.22.0", - "gaze": "^0.5.2", + "gaze": "^1.0.0", "jasmine": "^2.3.2", - "mongodb-runner": "3.1.15", - "nodemon": "^1.8.1", - "parse-server-gcs-adapter": "^1.0.0" + "mongodb-runner": "3.2.2", + "nodemon": "^1.8.1" }, "scripts": { "dev": "npm run build && node bin/dev", diff --git a/spec/AdapterLoader.spec.js b/spec/AdapterLoader.spec.js index 81e057c51f..250a8a7c49 100644 --- a/spec/AdapterLoader.spec.js +++ b/spec/AdapterLoader.spec.js @@ -2,7 +2,6 @@ var loadAdapter = require("../src/Adapters/AdapterLoader").loadAdapter; var FilesAdapter = require("parse-server-fs-adapter").default; var S3Adapter = require("parse-server-s3-adapter").default; -var GCSAdapter = require("parse-server-gcs-adapter").default; var ParsePushAdapter = require("parse-server-push-adapter").default; describe("AdapterLoader", ()=>{ @@ -114,13 +113,4 @@ describe("AdapterLoader", ()=>{ }).not.toThrow(); done(); }) - - it("should load GCSAdapter from direct passing", (done) => { - var gcsAdapter = new GCSAdapter("projectId", "path/to/keyfile", "bucket") - expect(() => { - var adapter = loadAdapter(gcsAdapter, FilesAdapter); - expect(adapter).toBe(gcsAdapter); - }).not.toThrow(); - done(); - }) }); From 6cfcb4ddf82ce00327105494a78fc27aa691ee8b Mon Sep 17 00:00:00 2001 From: North Date: Thu, 12 May 2016 06:22:29 +0800 Subject: [PATCH 28/48] Fix #1755 (#1756) * Add condition at limit = 0 * Add tests for installations with limit and count parameters --- spec/InstallationsRouter.spec.js | 96 ++++++++++++++++++++++++++++++ src/Routers/InstallationsRouter.js | 2 +- 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/spec/InstallationsRouter.spec.js b/spec/InstallationsRouter.spec.js index 924e799fd9..82416aa42f 100644 --- a/spec/InstallationsRouter.spec.js +++ b/spec/InstallationsRouter.spec.js @@ -71,4 +71,100 @@ describe('InstallationsRouter', () => { done(); }); }); + + it('query installations with limit = 0', (done) => { + var androidDeviceRequest = { + 'installationId': '12345678-abcd-abcd-abcd-123456789abc', + 'deviceType': 'android' + }; + var iosDeviceRequest = { + 'installationId': '12345678-abcd-abcd-abcd-123456789abd', + 'deviceType': 'ios' + }; + var request = { + config: config, + auth: auth.master(config), + body: {}, + query: { + limit: 0 + } + }; + + var router = new InstallationsRouter(); + rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest) + .then(() => { + return rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest); + }).then(() => { + return router.handleFind(request); + }).then((res) => { + var response = res.response; + expect(response.results.length).toEqual(0); + done(); + }); + }); + + it('query installations with count = 1', (done) => { + var androidDeviceRequest = { + 'installationId': '12345678-abcd-abcd-abcd-123456789abc', + 'deviceType': 'android' + }; + var iosDeviceRequest = { + 'installationId': '12345678-abcd-abcd-abcd-123456789abd', + 'deviceType': 'ios' + }; + var request = { + config: config, + auth: auth.master(config), + body: {}, + query: { + count: 1 + } + }; + + var router = new InstallationsRouter(); + rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest) + .then(() => { + return rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest); + }).then(() => { + return router.handleFind(request); + }).then((res) => { + var response = res.response; + expect(response.results.length).toEqual(2); + expect(response.count).toEqual(2); + done(); + }); + }); + + it('query installations with limit = 0 and count = 1', (done) => { + var androidDeviceRequest = { + 'installationId': '12345678-abcd-abcd-abcd-123456789abc', + 'deviceType': 'android' + }; + var iosDeviceRequest = { + 'installationId': '12345678-abcd-abcd-abcd-123456789abd', + 'deviceType': 'ios' + }; + var request = { + config: config, + auth: auth.master(config), + body: {}, + query: { + limit: 0, + count: 1 + } + }; + + var router = new InstallationsRouter(); + rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest) + .then(() => { + return rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest); + }).then(() => { + return router.handleFind(request); + }).then((res) => { + var response = res.response; + expect(response.results.length).toEqual(0); + expect(response.count).toEqual(2); + done(); + }); + }); }); diff --git a/src/Routers/InstallationsRouter.js b/src/Routers/InstallationsRouter.js index 372bba8140..4a9efc3f64 100644 --- a/src/Routers/InstallationsRouter.js +++ b/src/Routers/InstallationsRouter.js @@ -12,7 +12,7 @@ export class InstallationsRouter extends ClassesRouter { if (body.skip) { options.skip = Number(body.skip); } - if (body.limit) { + if (body.limit || body.limit === 0) { options.limit = Number(body.limit); } if (body.order) { From 19e7407f554e14aae8f928ffc159bd7e24d02637 Mon Sep 17 00:00:00 2001 From: Marco Cheung Date: Thu, 12 May 2016 08:24:15 +0800 Subject: [PATCH 29/48] Return correct error when violating unique index (#1763) --- spec/ParseAPI.spec.js | 22 +++++++++++++++++++ .../Storage/Mongo/MongoStorageAdapter.js | 9 +++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 084b7fe337..61efe8b397 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -5,6 +5,7 @@ var DatabaseAdapter = require('../src/DatabaseAdapter'); var request = require('request'); const Parse = require("parse/node"); +let Config = require('../src/Config'); describe('miscellaneous', function() { it('create a GameScore object', function(done) { @@ -1387,4 +1388,25 @@ describe('miscellaneous', function() { }) }); }); + + it('fail when create duplicate value in unique field', (done) => { + let obj = new Parse.Object('UniqueField'); + obj.set('unique', 'value'); + obj.save().then(() => { + expect(obj.id).not.toBeUndefined(); + let config = new Config('test'); + return config.database.adapter.adaptiveCollection('UniqueField') + }).then(collection => { + return collection._mongoCollection.createIndex({ 'unique': 1 }, { unique: true }) + }).then(() => { + let obj = new Parse.Object('UniqueField'); + obj.set('unique', 'value'); + return obj.save() + }).then(() => { + return Promise.reject(); + }, error => { + expect(error.code === Parse.Error.DUPLICATE_VALUE); + done(); + }); + }); }); diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index d61df40cbc..81af4e970c 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -166,7 +166,14 @@ export class MongoStorageAdapter { createObject(className, object, schemaController, parseFormatSchema) { const mongoObject = transform.parseObjectToMongoObjectForCreate(schemaController, className, object, parseFormatSchema); return this.adaptiveCollection(className) - .then(collection => collection.insertOne(mongoObject)); + .then(collection => collection.insertOne(mongoObject)) + .catch(error => { + if (error.code === 11000) { // Duplicate value + throw new Parse.Error(Parse.Error.DUPLICATE_VALUE, + 'A duplicate value for a field with unique values was provided'); + } + return Promise.reject(error); + }); } // Remove all objects that match the given parse query. Parse Query should be in Parse Format. From c2cfa14627971d10e9c4039e6c92ee073701e2b0 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Thu, 12 May 2016 16:30:38 -0700 Subject: [PATCH 30/48] Tell dashboard that the feature exits --- src/Routers/FeaturesRouter.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Routers/FeaturesRouter.js b/src/Routers/FeaturesRouter.js index cda604d41d..9c3ad7619b 100644 --- a/src/Routers/FeaturesRouter.js +++ b/src/Routers/FeaturesRouter.js @@ -39,6 +39,7 @@ export class FeaturesRouter extends PromiseRouter { clearAllDataFromClass: false, exportClass: false, editClassLevelPermissions: true, + editPointerPermissions: true, }, }; From d0c3535a395abbd95fbe58594a2e73b3b2b042e4 Mon Sep 17 00:00:00 2001 From: Marco Cheung Date: Sat, 14 May 2016 01:51:01 +0800 Subject: [PATCH 31/48] Fix error when unset user email (#1778) --- spec/ParseUser.spec.js | 16 ++++++++++++++++ src/RestWrite.js | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index 0873f4426f..ce9763af89 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -1834,6 +1834,22 @@ describe('Parse.User testing', () => { }); }); + it('unset user email', (done) => { + var user = new Parse.User(); + user.set('username', 'test'); + user.set('password', 'test'); + user.set('email', 'test@test.com'); + user.signUp().then(() => { + user.unset('email'); + return user.save(); + }).then(() => { + return Parse.User.logIn('test', 'test'); + }).then((user) => { + expect(user.getEmail()).toBeUndefined(); + done(); + }); + }); + it('create session from user', (done) => { Parse.Promise.as().then(() => { return Parse.User.signUp("finn", "human", { foo: "bar" }); diff --git a/src/RestWrite.js b/src/RestWrite.js index 03138e44f4..ecb92a85e4 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -356,7 +356,7 @@ RestWrite.prototype.transformUser = function() { return Promise.resolve(); }); }).then(() => { - if (!this.data.email) { + if (!this.data.email || this.data.email.__op === 'Delete') { return; } // Validate basic email address format From e4998c256a516d4a69553f43db5f95ec96dd257a Mon Sep 17 00:00:00 2001 From: Drew Date: Fri, 13 May 2016 15:28:14 -0700 Subject: [PATCH 32/48] Move field name validation logic out of mongo (#1752) * Remove transformKey(...) * Move validation logic into Parse Server and out of Mongo Adapter * Fix nits --- spec/MongoTransform.spec.js | 11 ----------- src/Adapters/Storage/Mongo/MongoTransform.js | 13 +------------ src/Controllers/DatabaseController.js | 19 ++++++++++++++++--- src/Controllers/SchemaController.js | 12 +++++++----- 4 files changed, 24 insertions(+), 31 deletions(-) diff --git a/spec/MongoTransform.spec.js b/spec/MongoTransform.spec.js index bae5805fad..755187dd9e 100644 --- a/spec/MongoTransform.spec.js +++ b/spec/MongoTransform.spec.js @@ -191,17 +191,6 @@ describe('untransformObject', () => { }); }); -describe('transformKey', () => { - it('throws out _password', (done) => { - try { - transform.transformKey(dummySchema, '_User', '_password'); - fail('should have thrown'); - } catch (e) { - done(); - } - }); -}); - describe('transform schema key changes', () => { it('changes new pointer key', (done) => { diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 0013a39d0e..d445e7cec2 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -15,14 +15,11 @@ var Parse = require('parse/node').Parse; // in the value are converted to a mongo update form. Otherwise they are // converted to static data. // -// validate: true indicates that key names are to be validated. -// // Returns an object with {key: key, value: value}. function transformKeyValue(schema, className, restKey, restValue, { inArray, inObject, update, - validate, } = {}) { // Check if the schema is known since it's a built-in field. var key = restKey; @@ -71,9 +68,6 @@ function transformKeyValue(schema, className, restKey, restValue, { if (authDataMatch) { 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); - } } // Handle special schema key changes @@ -454,11 +448,6 @@ function untransformACL(mongoObject) { return output; } -// Transforms a key used in the REST API format to its mongo format. -function transformKey(schema, className, key) { - return transformKeyValue(schema, className, key, null, {validate: true}).key; -} - // A sentinel value that helper transformations return when they // cannot perform a transformation function CannotTransform() {} @@ -1038,7 +1027,7 @@ var FileCoder = { }; module.exports = { - transformKey, + transformKeyValue, parseObjectToMongoObjectForCreate, transformUpdate, transformWhere, diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index de78895ed5..ab90fc3bfd 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -618,9 +618,22 @@ DatabaseController.prototype.find = function(className, query, { .then(schemaController => { if (sort) { mongoOptions.sort = {}; - for (let key in sort) { - let mongoKey = this.transform.transformKey(schemaController, className, key); - mongoOptions.sort[mongoKey] = sort[key]; + for (let fieldName in sort) { + // Parse.com treats queries on _created_at and _updated_at as if they were queries on createdAt and updatedAt, + // so duplicate that behaviour here. + if (fieldName === '_created_at') { + fieldName = 'createdAt'; + sort['createdAt'] = sort['_created_at']; + } else if (fieldName === '_updated_at') { + fieldName = 'updatedAt'; + sort['updatedAt'] = sort['_updated_at']; + } + + if (!SchemaController.fieldNameIsValid(fieldName)) { + throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name: ${fieldName}.`); + } + const mongoKey = this.transform.transformKeyValue(schemaController, className, fieldName, null).key; + mongoOptions.sort[mongoKey] = sort[fieldName]; } } return (isMaster ? Promise.resolve() : schemaController.validatePermission(className, aclGroup, op)) diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index af56610b99..8c78d1cf57 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -253,7 +253,7 @@ class SchemaController { this.data[schema.className] = schema.fields; this.perms[schema.className] = schema.classLevelPermissions; }); - + // Inject the in-memory classes volatileClasses.forEach(className => { this.data[className] = injectDefaultSchema({ @@ -466,15 +466,16 @@ class SchemaController { // If 'freeze' is true, refuse to update the schema for this field. validateField(className, fieldName, type, freeze) { return this.reloadData().then(() => { - // Just to check that the fieldName is valid - this._collection.transform.transformKey(this, className, fieldName); - - if( fieldName.indexOf(".") > 0 ) { + if (fieldName.indexOf(".") > 0) { // subdocument key (x.y) => ok if x is of type 'object' fieldName = fieldName.split(".")[ 0 ]; type = 'Object'; } + if (!fieldNameIsValid(fieldName)) { + throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name: ${fieldName}.`); + } + let expected = this.data[className][fieldName]; if (expected) { expected = (expected === 'map' ? 'Object' : expected); @@ -847,6 +848,7 @@ function getObjectType(obj) { export { load, classNameIsValid, + fieldNameIsValid, invalidClassNameMessage, buildMergedSchemaObject, systemClasses, From 1854928fe7c3e05be3524d9f647120969bcd46c0 Mon Sep 17 00:00:00 2001 From: Tyler Brock Date: Fri, 13 May 2016 18:17:22 -0700 Subject: [PATCH 33/48] Add test to ensure you can set ACL in beforeSave (#1772) --- spec/ParseAPI.spec.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 61efe8b397..68e0544bb6 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -225,6 +225,34 @@ describe('miscellaneous', function() { }); }); + it('test beforeSave set object acl success', function(done) { + var acl = new Parse.ACL({ + '*': { read: true, write: false } + }); + Parse.Cloud.beforeSave('BeforeSaveAddACL', function(req, res) { + req.object.setACL(acl); + res.success(); + }); + + var obj = new Parse.Object('BeforeSaveAddACL'); + obj.set('lol', true); + obj.save().then(function() { + Parse.Cloud._removeHook('Triggers', 'beforeSave', 'BeforeSaveAddACL'); + var query = new Parse.Query('BeforeSaveAddACL'); + query.get(obj.id).then(function(objAgain) { + expect(objAgain.get('lol')).toBeTruthy(); + expect(objAgain.getACL().equals(acl)); + done(); + }, function(error) { + fail(error); + done(); + }); + }, function(error) { + fail(error); + done(); + }); + }); + it('test beforeSave returns value on create and update', (done) => { var obj = new Parse.Object('BeforeSaveChanged'); obj.set('foo', 'bing'); From 3b4ae2d0a0025ae08b0b30e9b1f7ca9af882c0d9 Mon Sep 17 00:00:00 2001 From: Tyler Brock Date: Mon, 16 May 2016 14:41:25 -0700 Subject: [PATCH 34/48] Write old ACL format in _acl in addition to new format (#1810) --- spec/MongoTransform.spec.js | 15 +++++++++++++++ src/Adapters/Storage/Mongo/MongoTransform.js | 12 +++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/spec/MongoTransform.spec.js b/spec/MongoTransform.spec.js index 755187dd9e..905b7647c1 100644 --- a/spec/MongoTransform.spec.js +++ b/spec/MongoTransform.spec.js @@ -233,6 +233,21 @@ describe('transform schema key changes', () => { done(); }); + it('writes the old ACL format in addition to rperm and wperm', (done) => { + var input = { + ACL: { + "*": { "read": true }, + "Kevin": { "write": true } + } + }; + + var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input); + expect(typeof output._acl).toEqual('object'); + expect(output._acl["Kevin"].w).toBeTruthy(); + expect(output._acl["Kevin"].r).toBeUndefined(); + done(); + }) + it('untransforms from _rperm and _wperm to ACL', (done) => { var input = { _rperm: ["*"], diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index d445e7cec2..0cf09dbbee 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -348,7 +348,7 @@ function transformUpdate(schema, className, restUpdate) { var mongoUpdate = {}; var acl = transformACL(restUpdate); - if (acl._rperm || acl._wperm) { + if (acl._rperm || acl._wperm || acl._acl) { mongoUpdate['$set'] = {}; if (acl._rperm) { mongoUpdate['$set']['_rperm'] = acl._rperm; @@ -356,6 +356,9 @@ function transformUpdate(schema, className, restUpdate) { if (acl._wperm) { mongoUpdate['$set']['_wperm'] = acl._wperm; } + if (acl._acl) { + mongoUpdate['$set']['_acl'] = acl._acl; + } } for (var restKey in restUpdate) { @@ -404,16 +407,23 @@ function transformACL(restObject) { var acl = restObject['ACL']; var rperm = []; var wperm = []; + var _acl = {}; // old format + for (var entry in acl) { if (acl[entry].read) { rperm.push(entry); + _acl[entry] = _acl[entry] || {}; + _acl[entry]['r'] = true; } if (acl[entry].write) { wperm.push(entry); + _acl[entry] = _acl[entry] || {}; + _acl[entry]['w'] = true; } } output._rperm = rperm; output._wperm = wperm; + output._acl = _acl; delete restObject.ACL; return output; } From b40e16647bc811776107bbb1e5e6b2b3c1142112 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 16 May 2016 15:32:36 -0700 Subject: [PATCH 35/48] Changelog for version 2.2.10 --- CHANGELOG.md | 8 ++++++++ package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cecb204534..2d1dcf2a55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ ## Parse Server Changelog +### 2.2.10 (5/15/2016) + +* Fix: Write legacy ACLs to Mongo so that clients that still go through Parse.com can read them, thanks to [Tyler Brock](https://github.com/TylerBrock) and [carmenlau](https://github.com/carmenlau) +* Fix: Querying installations with limit = 0 and count = 1 now works, thanks to [ssk7833](https://github.com/ssk7833) +* Fix: Return correct error when violating unique index, thanks to [Marco Cheung](https://github.com/Marco129) +* Fix: Allow unsetting user's email, thanks to [Marco Cheung](https://github.com/Marco129) +* New: Support for Node 6.1 + ### 2.2.9 (5/9/2016) * Fix: Fix a regression that caused Parse Server to crash when a null parameter is passed to a Cloud function diff --git a/package.json b/package.json index 264f57ab6b..ac8bc7e037 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "2.2.9", + "version": "2.2.10", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From 40965186c05015c337b6be6e4beca8790fee6189 Mon Sep 17 00:00:00 2001 From: Marco Cheung Date: Wed, 18 May 2016 04:15:44 +0800 Subject: [PATCH 36/48] Mask sensitive information when logging (#1790) --- spec/FileLoggerAdapter.spec.js | 55 ++++++++++++++++++++++++++++++++++ src/PromiseRouter.js | 36 ++++++++++++++++++++-- 2 files changed, 89 insertions(+), 2 deletions(-) diff --git a/spec/FileLoggerAdapter.spec.js b/spec/FileLoggerAdapter.spec.js index fb4d6d5572..f259422df6 100644 --- a/spec/FileLoggerAdapter.spec.js +++ b/spec/FileLoggerAdapter.spec.js @@ -1,5 +1,8 @@ +'use strict'; + var FileLoggerAdapter = require('../src/Adapters/Logger/FileLoggerAdapter').FileLoggerAdapter; var Parse = require('parse/node').Parse; +var request = require('request'); describe('info logs', () => { @@ -45,3 +48,55 @@ describe('error logs', () => { }); }); }); + +describe('verbose logs', () => { + + it("mask sensitive information in _User class", (done) => { + let customConfig = Object.assign({}, defaultConfiguration, {verbose: true}); + setServerConfiguration(customConfig); + createTestUser().then(() => { + let fileLoggerAdapter = new FileLoggerAdapter(); + return fileLoggerAdapter.query({ + from: new Date(Date.now() - 500), + size: 100, + level: 'verbose' + }); + }).then((results) => { + expect(results[1].message.includes('"password": "********"')).toEqual(true); + var headers = { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest' + }; + request.get({ + headers: headers, + url: 'http://localhost:8378/1/login?username=test&password=moon-y' + }, (error, response, body) => { + let fileLoggerAdapter = new FileLoggerAdapter(); + return fileLoggerAdapter.query({ + from: new Date(Date.now() - 500), + size: 100, + level: 'verbose' + }).then((results) => { + expect(results[1].message.includes('password=********')).toEqual(true); + done(); + }); + }); + }); + }); + + it("should not mask information in non _User class", (done) => { + let obj = new Parse.Object('users'); + obj.set('password', 'pw'); + obj.save().then(() => { + let fileLoggerAdapter = new FileLoggerAdapter(); + return fileLoggerAdapter.query({ + from: new Date(Date.now() - 500), + size: 100, + level: 'verbose' + }); + }).then((results) => { + expect(results[1].message.includes('"password": "pw"')).toEqual(true); + done(); + }); + }); +}); diff --git a/src/PromiseRouter.js b/src/PromiseRouter.js index 099b2474b4..b159ef0768 100644 --- a/src/PromiseRouter.js +++ b/src/PromiseRouter.js @@ -6,6 +6,7 @@ // components that external developers may be modifying. import express from 'express'; +import url from 'url'; import log from './logger'; export default class PromiseRouter { @@ -154,8 +155,8 @@ export default class PromiseRouter { function makeExpressHandler(promiseHandler) { return function(req, res, next) { try { - log.verbose(req.method, req.originalUrl, req.headers, - JSON.stringify(req.body, null, 2)); + log.verbose(req.method, maskSensitiveUrl(req), req.headers, + JSON.stringify(maskSensitiveBody(req), null, 2)); promiseHandler(req).then((result) => { if (!result.response && !result.location && !result.text) { log.error('the handler did not include a "response" or a "location" field'); @@ -194,3 +195,34 @@ function makeExpressHandler(promiseHandler) { } } } + +function maskSensitiveBody(req) { + let maskBody = Object.assign({}, req.body); + let shouldMaskBody = (req.method === 'POST' && req.originalUrl.endsWith('/users') + && !req.originalUrl.includes('classes')) || + (req.method === 'PUT' && /users\/\w+$/.test(req.originalUrl) + && !req.originalUrl.includes('classes')) || + (req.originalUrl.includes('classes/_User')); + if (shouldMaskBody) { + for (let key of Object.keys(maskBody)) { + if (key == 'password') { + maskBody[key] = '********'; + break; + } + } + } + return maskBody; +} + +function maskSensitiveUrl(req) { + let maskUrl = req.originalUrl.toString(); + let shouldMaskUrl = req.method === 'GET' && req.originalUrl.includes('/login') + && !req.originalUrl.includes('classes'); + if (shouldMaskUrl) { + let password = url.parse(req.originalUrl, true).query.password; + if (password) { + maskUrl = maskUrl.replace('password=' + password, 'password=********') + } + } + return maskUrl; +} From 5d887e18f0dfdb22c8b4a70aa764f502ede2b881 Mon Sep 17 00:00:00 2001 From: KartikeyaRokde Date: Wed, 18 May 2016 05:32:28 +0530 Subject: [PATCH 37/48] FIX #1572 - Accepting LOGS_FOLDER as env variable (#1757) * FIX #1572 - Accepting LOGS_FOLDER as env variable * Changed env variable LOGS_FOLDER to PARSE_SERVER_LOGS_FOLDER * Added Note for starting parse-server with PARSE_SERVER_LOGS_FOLDER env variable --- README.md | 2 ++ src/logger.js | 2 ++ 2 files changed, 4 insertions(+) diff --git a/README.md b/README.md index de6b43f74c..586cc9d3b9 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,8 @@ That's it! You are now running a standalone version of Parse Server on your mach **Using a remote MongoDB?** Pass the `--databaseURI DATABASE_URI` parameter when starting `parse-server`. Learn more about configuring Parse Server [here](#configuration). For a full list of available options, run `parse-server --help`. +**Want logs to be in placed in other folder?** Pass the `PARSE_SERVER_LOGS_FOLDER` environment variable when starting `parse-server`. Usage :- `PARSE_SERVER_LOGS_FOLDER='' parse-server --appId APPLICATION_ID --masterKey MASTER_KEY` + ### Saving your first object Now that you're running Parse Server, it is time to save your first object. We'll use the [REST API](https://parse.com/docs/rest/guide), but you can easily do the same using any of the [Parse SDKs](https://parseplatform.github.io/#sdks). Run the following: diff --git a/src/logger.js b/src/logger.js index e0556bf7d9..d5b81e9ecf 100644 --- a/src/logger.js +++ b/src/logger.js @@ -9,6 +9,8 @@ if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') { LOGS_FOLDER = './test_logs/' } +LOGS_FOLDER = process.env.PARSE_SERVER_LOGS_FOLDER || LOGS_FOLDER; + let currentLogsFolder = LOGS_FOLDER; function generateTransports(level) { From 8c09c3dae1b913b336e5a55c7066e3de7b522087 Mon Sep 17 00:00:00 2001 From: Blayne Chard Date: Wed, 18 May 2016 12:12:30 +1200 Subject: [PATCH 38/48] Adding Caching Adapter, allows caching of _Role and _User queries (fixes #168) (#1664) * Adding Caching Adapter, allows caching of _Role and _User queries. --- spec/CacheController.spec.js | 74 ++++++++++++ spec/InMemoryCache.spec.js | 74 ++++++++++++ spec/InMemoryCacheAdapter.spec.js | 59 ++++++++++ spec/helper.js | 2 +- src/Adapters/Cache/CacheAdapter.js | 27 +++++ src/Adapters/Cache/InMemoryCache.js | 66 +++++++++++ src/Adapters/Cache/InMemoryCacheAdapter.js | 36 ++++++ src/Auth.js | 131 ++++++++++++--------- src/Config.js | 5 +- src/Controllers/CacheController.js | 75 ++++++++++++ src/DatabaseAdapter.js | 2 +- src/ParseServer.js | 19 ++- src/RestWrite.js | 27 ++--- src/cache.js | 37 +----- src/middlewares.js | 12 +- src/rest.js | 8 +- src/testing-routes.js | 4 +- src/triggers.js | 2 +- 18 files changed, 526 insertions(+), 134 deletions(-) create mode 100644 spec/CacheController.spec.js create mode 100644 spec/InMemoryCache.spec.js create mode 100644 spec/InMemoryCacheAdapter.spec.js create mode 100644 src/Adapters/Cache/CacheAdapter.js create mode 100644 src/Adapters/Cache/InMemoryCache.js create mode 100644 src/Adapters/Cache/InMemoryCacheAdapter.js create mode 100644 src/Controllers/CacheController.js diff --git a/spec/CacheController.spec.js b/spec/CacheController.spec.js new file mode 100644 index 0000000000..1e02d59e2f --- /dev/null +++ b/spec/CacheController.spec.js @@ -0,0 +1,74 @@ +var CacheController = require('../src/Controllers/CacheController.js').default; + +describe('CacheController', function() { + var FakeCacheAdapter; + var FakeAppID = 'foo'; + var KEY = 'hello'; + + beforeEach(() => { + FakeCacheAdapter = { + get: () => Promise.resolve(null), + put: jasmine.createSpy('put'), + del: jasmine.createSpy('del'), + clear: jasmine.createSpy('clear') + } + + spyOn(FakeCacheAdapter, 'get').and.callThrough(); + }); + + + it('should expose role and user caches', (done) => { + var cache = new CacheController(FakeCacheAdapter, FakeAppID); + + expect(cache.role).not.toEqual(null); + expect(cache.role.get).not.toEqual(null); + expect(cache.user).not.toEqual(null); + expect(cache.user.get).not.toEqual(null); + + done(); + }); + + + ['role', 'user'].forEach((cacheName) => { + it('should prefix ' + cacheName + ' cache', () => { + var cache = new CacheController(FakeCacheAdapter, FakeAppID)[cacheName]; + + cache.put(KEY, 'world'); + var firstPut = FakeCacheAdapter.put.calls.first(); + expect(firstPut.args[0]).toEqual([FakeAppID, cacheName, KEY].join(':')); + + cache.get(KEY); + var firstGet = FakeCacheAdapter.get.calls.first(); + expect(firstGet.args[0]).toEqual([FakeAppID, cacheName, KEY].join(':')); + + cache.del(KEY); + var firstDel = FakeCacheAdapter.del.calls.first(); + expect(firstDel.args[0]).toEqual([FakeAppID, cacheName, KEY].join(':')); + }); + }); + + it('should clear the entire cache', () => { + var cache = new CacheController(FakeCacheAdapter, FakeAppID); + + cache.clear(); + expect(FakeCacheAdapter.clear.calls.count()).toEqual(1); + + cache.user.clear(); + expect(FakeCacheAdapter.clear.calls.count()).toEqual(2); + + cache.role.clear(); + expect(FakeCacheAdapter.clear.calls.count()).toEqual(3); + }); + + it('should handle cache rejections', (done) => { + + FakeCacheAdapter.get = () => Promise.reject(); + + var cache = new CacheController(FakeCacheAdapter, FakeAppID); + + cache.get('foo').then(done, () => { + fail('Promise should not be rejected.'); + }); + }); + +}); diff --git a/spec/InMemoryCache.spec.js b/spec/InMemoryCache.spec.js new file mode 100644 index 0000000000..3c0fb47bbb --- /dev/null +++ b/spec/InMemoryCache.spec.js @@ -0,0 +1,74 @@ +const InMemoryCache = require('../src/Adapters/Cache/InMemoryCache').default; + + +describe('InMemoryCache', function() { + var BASE_TTL = { + ttl: 10 + }; + var NO_EXPIRE_TTL = { + ttl: NaN + }; + var KEY = 'hello'; + var KEY_2 = KEY + '_2'; + + var VALUE = 'world'; + + + function wait(sleep) { + return new Promise(function(resolve, reject) { + setTimeout(resolve, sleep); + }) + } + + it('should destroy a expire items in the cache', (done) => { + var cache = new InMemoryCache(BASE_TTL); + + cache.put(KEY, VALUE); + + var value = cache.get(KEY); + expect(value).toEqual(VALUE); + + wait(BASE_TTL.ttl * 5).then(() => { + value = cache.get(KEY) + expect(value).toEqual(null); + done(); + }); + }); + + it('should delete items', (done) => { + var cache = new InMemoryCache(NO_EXPIRE_TTL); + cache.put(KEY, VALUE); + cache.put(KEY_2, VALUE); + expect(cache.get(KEY)).toEqual(VALUE); + expect(cache.get(KEY_2)).toEqual(VALUE); + + cache.del(KEY); + expect(cache.get(KEY)).toEqual(null); + expect(cache.get(KEY_2)).toEqual(VALUE); + + cache.del(KEY_2); + expect(cache.get(KEY)).toEqual(null); + expect(cache.get(KEY_2)).toEqual(null); + done(); + }); + + it('should clear all items', (done) => { + var cache = new InMemoryCache(NO_EXPIRE_TTL); + cache.put(KEY, VALUE); + cache.put(KEY_2, VALUE); + + expect(cache.get(KEY)).toEqual(VALUE); + expect(cache.get(KEY_2)).toEqual(VALUE); + cache.clear(); + + expect(cache.get(KEY)).toEqual(null); + expect(cache.get(KEY_2)).toEqual(null); + done(); + }); + + it('should deafult TTL to 5 seconds', () => { + var cache = new InMemoryCache({}); + expect(cache.ttl).toEqual(5 * 1000); + }); + +}); diff --git a/spec/InMemoryCacheAdapter.spec.js b/spec/InMemoryCacheAdapter.spec.js new file mode 100644 index 0000000000..405da6f7ad --- /dev/null +++ b/spec/InMemoryCacheAdapter.spec.js @@ -0,0 +1,59 @@ +var InMemoryCacheAdapter = require('../src/Adapters/Cache/InMemoryCacheAdapter').default; + +describe('InMemoryCacheAdapter', function() { + var KEY = 'hello'; + var VALUE = 'world'; + + function wait(sleep) { + return new Promise(function(resolve, reject) { + setTimeout(resolve, sleep); + }) + } + + it('should expose promisifyed methods', (done) => { + var cache = new InMemoryCacheAdapter({ + ttl: NaN + }); + + var noop = () => {}; + + // Verify all methods return promises. + Promise.all([ + cache.put(KEY, VALUE), + cache.del(KEY), + cache.get(KEY), + cache.clear() + ]).then(() => { + done(); + }); + }); + + it('should get/set/clear', (done) => { + var cache = new InMemoryCacheAdapter({ + ttl: NaN + }); + + cache.put(KEY, VALUE) + .then(() => cache.get(KEY)) + .then((value) => expect(value).toEqual(VALUE)) + .then(() => cache.clear()) + .then(() => cache.get(KEY)) + .then((value) => expect(value).toEqual(null)) + .then(done); + }); + + it('should expire after ttl', (done) => { + var cache = new InMemoryCacheAdapter({ + ttl: 10 + }); + + cache.put(KEY, VALUE) + .then(() => cache.get(KEY)) + .then((value) => expect(value).toEqual(VALUE)) + .then(wait.bind(null, 50)) + .then(() => cache.get(KEY)) + .then((value) => expect(value).toEqual(null)) + .then(done); + }) + +}); diff --git a/spec/helper.js b/spec/helper.js index 0d6379fbf8..aa63ff0f4d 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -63,7 +63,7 @@ const setServerConfiguration = configuration => { DatabaseAdapter.clearDatabaseSettings(); currentConfiguration = configuration; server.close(); - cache.clearCache(); + cache.clear(); app = express(); api = new ParseServer(configuration); app.use('/1', api); diff --git a/src/Adapters/Cache/CacheAdapter.js b/src/Adapters/Cache/CacheAdapter.js new file mode 100644 index 0000000000..7d65381763 --- /dev/null +++ b/src/Adapters/Cache/CacheAdapter.js @@ -0,0 +1,27 @@ +export class CacheAdapter { + /** + * Get a value in the cache + * @param key Cache key to get + * @return Promise that will eventually resolve to the value in the cache. + */ + get(key) {} + + /** + * Set a value in the cache + * @param key Cache key to set + * @param value Value to set the key + * @param ttl Optional TTL + */ + put(key, value, ttl) {} + + /** + * Remove a value from the cache. + * @param key Cache key to remove + */ + del(key) {} + + /** + * Empty a cache + */ + clear() {} +} diff --git a/src/Adapters/Cache/InMemoryCache.js b/src/Adapters/Cache/InMemoryCache.js new file mode 100644 index 0000000000..37eeb43b02 --- /dev/null +++ b/src/Adapters/Cache/InMemoryCache.js @@ -0,0 +1,66 @@ +const DEFAULT_CACHE_TTL = 5 * 1000; + + +export class InMemoryCache { + constructor({ + ttl = DEFAULT_CACHE_TTL + }) { + this.ttl = ttl; + this.cache = Object.create(null); + } + + get(key) { + let record = this.cache[key]; + if (record == null) { + return null; + } + + // Has Record and isnt expired + if (isNaN(record.expire) || record.expire >= Date.now()) { + return record.value; + } + + // Record has expired + delete this.cache[key]; + return null; + } + + put(key, value, ttl = this.ttl) { + if (ttl < 0 || isNaN(ttl)) { + ttl = NaN; + } + + var record = { + value: value, + expire: ttl + Date.now() + } + + if (!isNaN(record.expire)) { + record.timeout = setTimeout(() => { + this.del(key); + }, ttl); + } + + this.cache[key] = record; + } + + del(key) { + var record = this.cache[key]; + if (record == null) { + return; + } + + if (record.timeout) { + clearTimeout(record.timeout); + } + + delete this.cache[key]; + } + + clear() { + this.cache = Object.create(null); + } + +} + +export default InMemoryCache; diff --git a/src/Adapters/Cache/InMemoryCacheAdapter.js b/src/Adapters/Cache/InMemoryCacheAdapter.js new file mode 100644 index 0000000000..09e1c12a11 --- /dev/null +++ b/src/Adapters/Cache/InMemoryCacheAdapter.js @@ -0,0 +1,36 @@ +import {InMemoryCache} from './InMemoryCache'; + +export class InMemoryCacheAdapter { + + constructor(ctx) { + this.cache = new InMemoryCache(ctx) + } + + get(key) { + return new Promise((resolve, reject) => { + let record = this.cache.get(key); + if (record == null) { + return resolve(null); + } + + return resolve(JSON.parse(record)); + }) + } + + put(key, value, ttl) { + this.cache.put(key, JSON.stringify(value), ttl); + return Promise.resolve(); + } + + del(key) { + this.cache.del(key); + return Promise.resolve(); + } + + clear() { + this.cache.clear(); + return Promise.resolve(); + } +} + +export default InMemoryCacheAdapter; diff --git a/src/Auth.js b/src/Auth.js index f21bdc763f..8f21567903 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -2,8 +2,6 @@ var deepcopy = require('deepcopy'); var Parse = require('parse/node').Parse; var RestQuery = require('./RestQuery'); -import cache from './cache'; - // An Auth object tells you who is requesting something and whether // the master key was used. // userObject is a Parse.User and can be null if there's no user. @@ -42,36 +40,42 @@ function nobody(config) { return new Auth({ config, isMaster: false }); } + // Returns a promise that resolves to an Auth object var getAuthForSessionToken = function({ config, sessionToken, installationId } = {}) { - var cachedUser = cache.users.get(sessionToken); - if (cachedUser) { - return Promise.resolve(new Auth({ config, isMaster: false, installationId, user: cachedUser })); - } - var restOptions = { - limit: 1, - include: 'user' - }; - 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']) { - return nobody(config); + return config.cacheController.user.get(sessionToken).then((userJSON) => { + if (userJSON) { + let cachedUser = Parse.Object.fromJSON(userJSON); + return Promise.resolve(new Auth({config, isMaster: false, installationId, user: cachedUser})); } - var now = new Date(), + var restOptions = { + limit: 1, + include: 'user' + }; + + 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']) { + return nobody(config); + } + + var now = new Date(), expiresAt = results[0].expiresAt ? new Date(results[0].expiresAt.iso) : undefined; - if(expiresAt < now) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, - 'Session token is expired.'); - } - var obj = results[0]['user']; - delete obj.password; - obj['className'] = '_User'; - obj['sessionToken'] = sessionToken; - let userObject = Parse.Object.fromJSON(obj); - cache.users.set(sessionToken, userObject); - return new Auth({ config, isMaster: false, installationId, user: userObject }); + if (expiresAt < now) { + throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, + 'Session token is expired.'); + } + var obj = results[0]['user']; + delete obj.password; + obj['className'] = '_User'; + obj['sessionToken'] = sessionToken; + config.cacheController.user.put(sessionToken, obj); + + let userObject = Parse.Object.fromJSON(obj); + return new Auth({config, isMaster: false, installationId, user: userObject}); + }); }); }; @@ -92,39 +96,50 @@ Auth.prototype.getUserRoles = function() { // Iterates through the role tree and compiles a users roles Auth.prototype._loadRoles = function() { - var restWhere = { - 'users': { - __type: 'Pointer', - className: '_User', - objectId: this.user.id - } - }; - // First get the role ids this user is directly a member of - var query = new RestQuery(this.config, master(this.config), '_Role', - restWhere, {}); - return query.execute().then((response) => { - var results = response.results; - if (!results.length) { - this.userRoles = []; - this.fetchedRoles = true; - this.rolePromise = null; - return Promise.resolve(this.userRoles); + var cacheAdapter = this.config.cacheController; + return cacheAdapter.role.get(this.user.id).then((cachedRoles) => { + if (cachedRoles != null) { + this.fetchedroles = true; + return Promise.resolve(cachedRoles); } - var rolesMap = results.reduce((m, r) => { - m.names.push(r.name); - m.ids.push(r.objectId); - return m; - }, {ids: [], names: []}); - // run the recursive finding - return this._getAllRolesNamesForRoleIds(rolesMap.ids, rolesMap.names) - .then((roleNames) => { - this.userRoles = roleNames.map((r) => { - return 'role:' + r; - }); - this.fetchedRoles = true; - this.rolePromise = null; - return Promise.resolve(this.userRoles); + var restWhere = { + 'users': { + __type: 'Pointer', + className: '_User', + objectId: this.user.id + } + }; + // First get the role ids this user is directly a member of + var query = new RestQuery(this.config, master(this.config), '_Role', restWhere, {}); + return query.execute().then((response) => { + var results = response.results; + if (!results.length) { + this.userRoles = []; + this.fetchedRoles = true; + this.rolePromise = null; + + cacheAdapter.role.put(this.user.id, this.userRoles); + return Promise.resolve(this.userRoles); + } + var rolesMap = results.reduce((m, r) => { + m.names.push(r.name); + m.ids.push(r.objectId); + return m; + }, {ids: [], names: []}); + + // run the recursive finding + return this._getAllRolesNamesForRoleIds(rolesMap.ids, rolesMap.names) + .then((roleNames) => { + this.userRoles = roleNames.map((r) => { + return 'role:' + r; + }); + this.fetchedRoles = true; + this.rolePromise = null; + + cacheAdapter.role.put(this.user.id, this.userRoles); + return Promise.resolve(this.userRoles); + }); }); }); }; diff --git a/src/Config.js b/src/Config.js index badff9da40..faaa6c2235 100644 --- a/src/Config.js +++ b/src/Config.js @@ -2,7 +2,7 @@ // configured. // mount is the URL for the root of the API; includes http, domain, etc. -import cache from './cache'; +import AppCache from './cache'; function removeTrailingSlash(str) { if (!str) { @@ -17,7 +17,7 @@ function removeTrailingSlash(str) { export class Config { constructor(applicationId: string, mount: string) { let DatabaseAdapter = require('./DatabaseAdapter'); - let cacheInfo = cache.apps.get(applicationId); + let cacheInfo = AppCache.get(applicationId); if (!cacheInfo) { return; } @@ -38,6 +38,7 @@ export class Config { this.verifyUserEmails = cacheInfo.verifyUserEmails; this.appName = cacheInfo.appName; + this.cacheController = cacheInfo.cacheController; this.hooksController = cacheInfo.hooksController; this.filesController = cacheInfo.filesController; this.pushController = cacheInfo.pushController; diff --git a/src/Controllers/CacheController.js b/src/Controllers/CacheController.js new file mode 100644 index 0000000000..27dc4936f7 --- /dev/null +++ b/src/Controllers/CacheController.js @@ -0,0 +1,75 @@ +import AdaptableController from './AdaptableController'; +import CacheAdapter from '../Adapters/Cache/CacheAdapter'; + +const KEY_SEPARATOR_CHAR = ':'; + +function joinKeys(...keys) { + return keys.join(KEY_SEPARATOR_CHAR); +} + +/** + * Prefix all calls to the cache via a prefix string, useful when grouping Cache by object type. + * + * eg "Role" or "Session" + */ +export class SubCache { + constructor(prefix, cacheController) { + this.prefix = prefix; + this.cache = cacheController; + } + + get(key) { + let cacheKey = joinKeys(this.prefix, key); + return this.cache.get(cacheKey); + } + + put(key, value, ttl) { + let cacheKey = joinKeys(this.prefix, key); + return this.cache.put(cacheKey, value, ttl); + } + + del(key) { + let cacheKey = joinKeys(this.prefix, key); + return this.cache.del(cacheKey); + } + + clear() { + return this.cache.clear(); + } +} + + +export class CacheController extends AdaptableController { + + constructor(adapter, appId, options = {}) { + super(adapter, appId, options); + + this.role = new SubCache('role', this); + this.user = new SubCache('user', this); + } + + get(key) { + let cacheKey = joinKeys(this.appId, key); + return this.adapter.get(cacheKey).then(null, () => Promise.resolve(null)); + } + + put(key, value, ttl) { + let cacheKey = joinKeys(this.appId, key); + return this.adapter.put(cacheKey, value, ttl); + } + + del(key) { + let cacheKey = joinKeys(this.appId, key); + return this.adapter.del(cacheKey); + } + + clear() { + return this.adapter.clear(); + } + + expectedAdapterType() { + return CacheAdapter; + } +} + +export default CacheController; diff --git a/src/DatabaseAdapter.js b/src/DatabaseAdapter.js index 73bc09334f..25ce69b422 100644 --- a/src/DatabaseAdapter.js +++ b/src/DatabaseAdapter.js @@ -60,7 +60,7 @@ function getDatabaseConnection(appId: string, collectionPrefix: string) { uri: appDatabaseURIs[appId], //may be undefined if the user didn't supply a URI, in which case the default will be used } - dbConnections[appId] = new DatabaseController(new MongoStorageAdapter(mongoAdapterOptions)); + dbConnections[appId] = new DatabaseController(new MongoStorageAdapter(mongoAdapterOptions), {appId: appId}); return dbConnections[appId]; } diff --git a/src/ParseServer.js b/src/ParseServer.js index baeb1d69e5..85b2b40c02 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -15,8 +15,8 @@ if (!global._babelPolyfill) { } import { logger, - configureLogger } from './logger'; -import cache from './cache'; + configureLogger } from './logger'; +import AppCache from './cache'; import Config from './Config'; import parseServerPackage from '../package.json'; import PromiseRouter from './PromiseRouter'; @@ -24,6 +24,8 @@ import requiredParameter from './requiredParameter'; import { AnalyticsRouter } from './Routers/AnalyticsRouter'; import { ClassesRouter } from './Routers/ClassesRouter'; import { FeaturesRouter } from './Routers/FeaturesRouter'; +import { InMemoryCacheAdapter } from './Adapters/Cache/InMemoryCacheAdapter'; +import { CacheController } from './Controllers/CacheController'; import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter'; import { FilesController } from './Controllers/FilesController'; import { FilesRouter } from './Routers/FilesRouter'; @@ -104,6 +106,7 @@ class ParseServer { serverURL = requiredParameter('You must provide a serverURL!'), maxUploadSize = '20mb', verifyUserEmails = false, + cacheAdapter, emailAdapter, publicServerURL, customPages = { @@ -156,6 +159,8 @@ class ParseServer { const pushControllerAdapter = loadAdapter(push && push.adapter, ParsePushAdapter, push); const loggerControllerAdapter = loadAdapter(loggerAdapter, FileLoggerAdapter); const emailControllerAdapter = loadAdapter(emailAdapter); + const cacheControllerAdapter = loadAdapter(cacheAdapter, InMemoryCacheAdapter, {appId: appId}); + // We pass the options and the base class for the adatper, // Note that passing an instance would work too const filesController = new FilesController(filesControllerAdapter, appId); @@ -164,8 +169,9 @@ class ParseServer { const hooksController = new HooksController(appId, collectionPrefix); const userController = new UserController(emailControllerAdapter, appId, { verifyUserEmails }); const liveQueryController = new LiveQueryController(liveQuery); + const cacheController = new CacheController(cacheControllerAdapter, appId); - cache.apps.set(appId, { + AppCache.put(appId, { masterKey: masterKey, serverURL: serverURL, collectionPrefix: collectionPrefix, @@ -175,6 +181,7 @@ class ParseServer { restAPIKey: restAPIKey, fileKey: fileKey, facebookAppIds: facebookAppIds, + cacheController: cacheController, filesController: filesController, pushController: pushController, loggerController: loggerController, @@ -195,11 +202,11 @@ class ParseServer { // To maintain compatibility. TODO: Remove in some version that breaks backwards compatability if (process.env.FACEBOOK_APP_ID) { - cache.apps.get(appId)['facebookAppIds'].push(process.env.FACEBOOK_APP_ID); + AppCache.get(appId)['facebookAppIds'].push(process.env.FACEBOOK_APP_ID); } - Config.validate(cache.apps.get(appId)); - this.config = cache.apps.get(appId); + Config.validate(AppCache.get(appId)); + this.config = AppCache.get(appId); hooksController.load(); } diff --git a/src/RestWrite.js b/src/RestWrite.js index ecb92a85e4..f6e758f6bf 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -2,7 +2,6 @@ // that writes to the database. // This could be either a "create" or an "update". -import cache from './cache'; var SchemaController = require('./Controllers/SchemaController'); var deepcopy = require('deepcopy'); @@ -310,6 +309,7 @@ RestWrite.prototype.handleAuthData = function(authData) { }); } + // The non-third-party parts of User transformation RestWrite.prototype.transformUser = function() { if (this.className !== '_User') { @@ -320,7 +320,8 @@ RestWrite.prototype.transformUser = function() { // If we're updating a _User object, clear the user cache for the session if (this.query && this.auth.user && this.auth.user.getSessionToken()) { - cache.users.remove(this.auth.user.getSessionToken()); + let cacheAdapter = this.config.cacheController; + cacheAdapter.user.del(this.auth.user.getSessionToken()); } return promise.then(() => { @@ -441,24 +442,6 @@ RestWrite.prototype.handleFollowup = function() { } }; -// Handles the _Role class specialness. -// Does nothing if this isn't a role object. -RestWrite.prototype.handleRole = function() { - if (this.response || this.className !== '_Role') { - return; - } - - if (!this.auth.user && !this.auth.isMaster) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, - 'Session token required.'); - } - - if (!this.data.name) { - throw new Parse.Error(Parse.Error.INVALID_ROLE_NAME, - 'Invalid role name.'); - } -}; - // Handles the _Session class specialness. // Does nothing if this isn't an installation object. RestWrite.prototype.handleSession = function() { @@ -716,6 +699,10 @@ RestWrite.prototype.runDatabaseOperation = function() { return; } + if (this.className === '_Role') { + this.config.cacheController.role.clear(); + } + if (this.className === '_User' && this.query && !this.auth.couldUpdateUserId(this.query.objectId)) { diff --git a/src/cache.js b/src/cache.js index 8893f29b1b..96b00b4534 100644 --- a/src/cache.js +++ b/src/cache.js @@ -1,35 +1,4 @@ -/** @flow weak */ +import {InMemoryCache} from './Adapters/Cache/InMemoryCache'; -export function CacheStore() { - let dataStore: {[id:KeyType]:ValueType} = {}; - return { - get: (key: KeyType): ValueType => { - return dataStore[key]; - }, - set(key: KeyType, value: ValueType): void { - dataStore[key] = value; - }, - remove(key: KeyType): void { - delete dataStore[key]; - }, - clear(): void { - dataStore = {}; - } - }; -} - -const apps = CacheStore(); -const users = CacheStore(); - -//So far used only in tests -export function clearCache(): void { - apps.clear(); - users.clear(); -} - -export default { - apps, - users, - clearCache, - CacheStore -}; +export var AppCache = new InMemoryCache({ttl: NaN}); +export default AppCache; diff --git a/src/middlewares.js b/src/middlewares.js index 10115d6853..c500609363 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -1,5 +1,5 @@ -import cache from './cache'; -import log from './logger'; +import AppCache from './cache'; +import log from './logger'; var Parse = require('parse/node').Parse; @@ -36,7 +36,7 @@ function handleParseHeaders(req, res, next) { var fileViaJSON = false; - if (!info.appId || !cache.apps.get(info.appId)) { + if (!info.appId || !AppCache.get(info.appId)) { // See if we can find the app id on the body. if (req.body instanceof Buffer) { // The only chance to find the app id is if this is a file @@ -51,8 +51,8 @@ function handleParseHeaders(req, res, next) { if (req.body && req.body._ApplicationId && - cache.apps.get(req.body._ApplicationId) && - (!info.masterKey || cache.apps.get(req.body._ApplicationId).masterKey === info.masterKey) + AppCache.get(req.body._ApplicationId) && + (!info.masterKey || AppCache.get(req.body._ApplicationId).masterKey === info.masterKey) ) { info.appId = req.body._ApplicationId; info.javascriptKey = req.body._JavaScriptKey || ''; @@ -87,7 +87,7 @@ function handleParseHeaders(req, res, next) { req.body = new Buffer(base64, 'base64'); } - info.app = cache.apps.get(info.appId); + info.app = AppCache.get(info.appId); req.config = new Config(info.appId, mount); req.info = info; diff --git a/src/rest.js b/src/rest.js index 60f017213e..d1894165a6 100644 --- a/src/rest.js +++ b/src/rest.js @@ -8,8 +8,7 @@ // things. var Parse = require('parse/node').Parse; -import cache from './cache'; -import Auth from './Auth'; +import Auth from './Auth'; var RestQuery = require('./RestQuery'); var RestWrite = require('./RestWrite'); @@ -48,7 +47,9 @@ function del(config, auth, className, objectId) { .then((response) => { if (response && response.results && response.results.length) { response.results[0].className = className; - cache.users.remove(response.results[0].sessionToken); + + var cacheAdapter = config.cacheController; + cacheAdapter.user.del(response.results[0].sessionToken); inflatedObject = Parse.Object.fromJSON(response.results[0]); // Notify LiveQuery server if possible config.liveQueryController.onAfterDelete(inflatedObject.className, inflatedObject); @@ -96,6 +97,7 @@ function create(config, auth, className, restObject) { // Usually, this is just updatedAt. function update(config, auth, className, objectId, restObject) { enforceRoleSecurity('update', className, auth); + return Promise.resolve().then(() => { if (triggers.getTrigger(className, triggers.Types.beforeSave, config.applicationId) || triggers.getTrigger(className, triggers.Types.afterSave, config.applicationId) || diff --git a/src/testing-routes.js b/src/testing-routes.js index 04b5cf83ed..eee022d9f9 100644 --- a/src/testing-routes.js +++ b/src/testing-routes.js @@ -1,5 +1,5 @@ // testing-routes.js -import cache from './cache'; +import AppCache from './cache'; import * as middlewares from './middlewares'; import { ParseServer } from './index'; import { Parse } from 'parse/node'; @@ -47,7 +47,7 @@ function dropApp(req, res) { return res.status(401).send({ "error": "unauthorized" }); } return req.config.database.deleteEverything().then(() => { - cache.apps.remove(req.config.applicationId); + AppCache.del(req.config.applicationId); res.status(200).send({}); }); } diff --git a/src/triggers.js b/src/triggers.js index 7ab1ea1902..c7c9ba2c1c 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -1,6 +1,6 @@ // triggers.js import Parse from 'parse/node'; -import cache from './cache'; +import AppCache from './cache'; export const Types = { beforeSave: 'beforeSave', From d7d46998322fc8f241863a57b1295986ae3730ab Mon Sep 17 00:00:00 2001 From: Tyler Brock Date: Wed, 18 May 2016 12:33:55 -0700 Subject: [PATCH 39/48] Fix logic for missing geo index error message check (#1824) --- src/Adapters/Storage/Mongo/MongoCollection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Adapters/Storage/Mongo/MongoCollection.js b/src/Adapters/Storage/Mongo/MongoCollection.js index e29b68754b..bf41582b19 100644 --- a/src/Adapters/Storage/Mongo/MongoCollection.js +++ b/src/Adapters/Storage/Mongo/MongoCollection.js @@ -17,7 +17,7 @@ export default class MongoCollection { return this._rawFind(query, { skip, limit, sort }) .catch(error => { // Check for "no geoindex" error - if (error.code != 17007 || !error.message.match(/unable to find index for .geoNear/)) { + if (error.code != 17007 && !error.message.match(/unable to find index for .geoNear/)) { throw error; } // Figure out what key needs an index From 4d4361451cdefa2592b92e8fd54c459b03190205 Mon Sep 17 00:00:00 2001 From: Drew Date: Wed, 18 May 2016 18:14:54 -0700 Subject: [PATCH 40/48] Refactor MongoTransform.js (#1823) * Split transformAtom into transfromTopLevelAtom and transformInteriorAtom * Use single param for inArray and inObject * Tidyness in transformKeyValue * Add transformInteriorKeyValue * Remove update from tranformInteriorKeyValue * Split out transform update * Move validation out of transfromUpdate * Remove force paramater from transformTopLevelAtom throw error after if necessary * Turn transformKeyValue into transfromKey since it is only used for that purpose * Remove unnecessary stuff from transformKey * convert transformKey to use parse format schema * interior keys fixes * Add test for interior keys with special names * Correct validation of inner keys --- spec/ParseAPI.spec.js | 32 +++ src/Adapters/Storage/Mongo/MongoTransform.js | 251 +++++++++---------- src/Controllers/DatabaseController.js | 99 ++++---- 3 files changed, 205 insertions(+), 177 deletions(-) diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 68e0544bb6..5f44dca79f 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -1437,4 +1437,36 @@ describe('miscellaneous', function() { done(); }); }); + + it('doesnt convert interior keys of objects that use special names', done => { + let obj = new Parse.Object('Obj'); + obj.set('val', { createdAt: 'a', updatedAt: 1 }); + obj.save() + .then(obj => new Parse.Query('Obj').get(obj.id)) + .then(obj => { + expect(obj.get('val').createdAt).toEqual('a'); + expect(obj.get('val').updatedAt).toEqual(1); + done(); + }); + }); + + it('bans interior keys containing . or $', done => { + new Parse.Object('Obj').save({innerObj: {'key with a $': 'fails'}}) + .catch(error => { + expect(error.code).toEqual(Parse.Error.INVALID_NESTED_KEY); + return new Parse.Object('Obj').save({innerObj: {'key with a .': 'fails'}}); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.INVALID_NESTED_KEY); + return new Parse.Object('Obj').save({innerObj: {innerInnerObj: {'key with $': 'fails'}}}); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.INVALID_NESTED_KEY); + return new Parse.Object('Obj').save({innerObj: {innerInnerObj: {'key with .': 'fails'}}}); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.INVALID_NESTED_KEY); + done(); + }) + }); }); diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 0cf09dbbee..31c3a9192c 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -3,24 +3,23 @@ import _ from 'lodash'; var mongodb = require('mongodb'); var Parse = require('parse/node').Parse; -// Transforms a key-value pair from REST API form to Mongo form. -// This is the main entry point for converting anything from REST form -// to Mongo form; no conversion should happen that doesn't pass -// through this function. -// Schema should already be loaded. -// -// There are several options that can help transform: -// -// 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. -// -// Returns an object with {key: key, value: value}. -function transformKeyValue(schema, className, restKey, restValue, { - inArray, - inObject, - update, -} = {}) { +const transformKey = (className, fieldName, schema) => { + // Check if the schema is known since it's a built-in field. + switch(fieldName) { + case 'objectId': return '_id'; + case 'createdAt': return '_created_at'; + case 'updatedAt': return '_updated_at'; + case 'sessionToken': return '_session_token'; + } + + if (schema.fields[fieldName] && schema.fields[fieldName].__type == 'Pointer') { + fieldName = '_p_' + fieldName; + } + + return fieldName; +} + +const transformKeyValueForUpdate = (schema, className, restKey, restValue) => { // Check if the schema is known since it's a built-in field. var key = restKey; var timeField = false; @@ -77,51 +76,60 @@ function transformKeyValue(schema, className, restKey, restValue, { if (schema && schema.getExpectedType) { expected = schema.getExpectedType(className, key); } - if ((expected && expected.type == 'Pointer') || - (!expected && restValue && restValue.__type == 'Pointer')) { + if ((expected && expected.type == 'Pointer') || (!expected && restValue && restValue.__type == 'Pointer')) { key = '_p_' + key; } - var expectedTypeIsArray = (expected && expected.type === 'Array'); // Handle atomic values - var value = transformAtom(restValue, false, { inArray, inObject }); + var value = transformTopLevelAtom(restValue); if (value !== CannotTransform) { if (timeField && (typeof value === 'string')) { value = new Date(value); } - return {key: key, value: value}; + return {key, value}; } - // ACLs are handled before this method is called - // If an ACL key still exists here, something is wrong. - if (key === 'ACL') { - throw 'There was a problem transforming an ACL.'; + // Handle arrays + if (restValue instanceof Array) { + value = restValue.map(transformInteriorValue); + return {key, value}; + } + + // Handle update operators + if (typeof restValue === 'object' && '__op' in restValue) { + return {key, value: transformUpdateOperator(restValue, false)}; + } + + // Handle normal objects by recursing + if (Object.keys(restValue).some(key => key.includes('$') || key.includes('.'))) { + throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters"); + } + value = _.mapValues(restValue, transformInteriorValue); + return {key, value}; +} + +const transformInteriorValue = restValue => { + if (typeof restValue === 'object' && Object.keys(restValue).some(key => key.includes('$') || key.includes('.'))) { + throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters"); + } + // Handle atomic values + var value = transformInteriorAtom(restValue); + if (value !== CannotTransform) { + return value; } // Handle arrays if (restValue instanceof Array) { - value = restValue.map((restObj) => { - var out = transformKeyValue(schema, className, restKey, restObj, { inArray: true }); - return out.value; - }); - return {key: key, value: value}; + return restValue.map(transformInteriorValue); } // Handle update operators - value = transformUpdateOperator(restValue, !update); - if (value !== CannotTransform) { - return {key: key, value: value}; + if (typeof restValue === 'object' && '__op' in restValue) { + return transformUpdateOperator(restValue, true); } // Handle normal objects by recursing - value = {}; - for (var subRestKey in restValue) { - var subRestValue = restValue[subRestKey]; - var out = transformKeyValue(schema, className, subRestKey, subRestValue, { inObject: true }); - // For recursed objects, keep the keys in rest format - value[subRestKey] = out.value; - } - return {key: key, value: value}; + return _.mapValues(restValue, transformInteriorValue); } const valueAsDate = value => { @@ -205,8 +213,8 @@ function transformQueryKeyValue(className, key, value, { validate } = {}, schema } // Handle atomic values - if (transformAtom(value, false) !== CannotTransform) { - return {key, value: transformAtom(value, false)}; + if (transformTopLevelAtom(value) !== CannotTransform) { + return {key, value: transformTopLevelAtom(value)}; } else { throw new Parse.Error(Parse.Error.INVALID_JSON, `You cannot use ${value} as a query parameter.`); } @@ -241,15 +249,15 @@ const parseObjectKeyValueToMongoObjectKeyValue = ( switch(restKey) { case 'objectId': return {key: '_id', value: restValue}; case 'createdAt': - transformedValue = transformAtom(restValue, false); + transformedValue = transformTopLevelAtom(restValue); coercedToDate = typeof transformedValue === 'string' ? new Date(transformedValue) : transformedValue return {key: '_created_at', value: coercedToDate}; case 'updatedAt': - transformedValue = transformAtom(restValue, false); + transformedValue = transformTopLevelAtom(restValue); coercedToDate = typeof transformedValue === 'string' ? new Date(transformedValue) : transformedValue return {key: '_updated_at', value: coercedToDate}; case 'expiresAt': - transformedValue = transformAtom(restValue, false); + transformedValue = transformTopLevelAtom(restValue); coercedToDate = typeof transformedValue === 'string' ? new Date(transformedValue) : transformedValue return {key: 'expiresAt', value: coercedToDate}; case '_rperm': @@ -268,7 +276,7 @@ const parseObjectKeyValueToMongoObjectKeyValue = ( return {key: restKey, value: restValue}; } } - //skip straight to transformAtom for Bytes, they don't show up in the schema for some reason + //skip straight to transformTopLevelAtom for Bytes, they don't show up in the schema for some reason if (restValue && restValue.__type !== 'Bytes') { //Note: We may not know the type of a field here, as the user could be saving (null) to a field //That never existed before, meaning we can't infer the type. @@ -278,7 +286,7 @@ const parseObjectKeyValueToMongoObjectKeyValue = ( } // Handle atomic values - var value = transformAtom(restValue, false, { inArray: false, inObject: false }); + var value = transformTopLevelAtom(restValue); if (value !== CannotTransform) { return {key: restKey, value: value}; } @@ -291,28 +299,21 @@ const parseObjectKeyValueToMongoObjectKeyValue = ( // Handle arrays if (restValue instanceof Array) { - value = restValue.map((restObj) => { - var out = transformKeyValue(schema, className, restKey, restObj, { inArray: true }); - return out.value; - }); + value = restValue.map(transformInteriorValue); return {key: restKey, value: value}; } // Handle update operators. TODO: handle within Parse Server. DB adapter shouldn't see update operators in creates. - value = transformUpdateOperator(restValue, true); - if (value !== CannotTransform) { - return {key: restKey, value: value}; + if (typeof restValue === 'object' && '__op' in restValue) { + return {key: restKey, value: transformUpdateOperator(restValue, true)}; } // Handle normal objects by recursing - value = {}; - for (var subRestKey in restValue) { - var subRestValue = restValue[subRestKey]; - var out = transformKeyValue(schema, className, subRestKey, subRestValue, { inObject: true }); - // For recursed objects, keep the keys in rest format - value[subRestKey] = out.value; + if (Object.keys(restValue).some(key => key.includes('$') || key.includes('.'))) { + throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters"); } - return {key: restKey, value: value}; + value = _.mapValues(restValue, transformInteriorValue); + return {key: restKey, value}; } // Main exposed method to create new objects. @@ -362,13 +363,12 @@ function transformUpdate(schema, className, restUpdate) { } for (var restKey in restUpdate) { - var out = transformKeyValue(schema, className, restKey, restUpdate[restKey], {update: true}); + var out = transformKeyValueForUpdate(schema, className, restKey, restUpdate[restKey]); // If the output value is an object with any $ keys, it's an // operator that needs to be lifted onto the top level update // object. - if (typeof out.value === 'object' && out.value !== null && - out.value.__op) { + if (typeof out.value === 'object' && out.value !== null && out.value.__op) { mongoUpdate[out.value.__op] = mongoUpdate[out.value.__op] || {}; mongoUpdate[out.value.__op][out.key] = out.value.arg; } else { @@ -462,20 +462,33 @@ function untransformACL(mongoObject) { // cannot perform a transformation function CannotTransform() {} +const transformInteriorAtom = atom => { + // TODO: check validity harder for the __type-defined types + if (typeof atom === 'object' && atom && !(atom instanceof Date) && atom.__type === 'Pointer') { + return { + __type: 'Pointer', + className: atom.className, + objectId: atom.objectId + }; + } else if (typeof atom === 'function' || typeof atom === 'symbol') { + throw new Parse.Error(Parse.Error.INVALID_JSON, `cannot transform value: ${atom}`); + } else if (DateCoder.isValidJSON(atom)) { + return DateCoder.JSONToDatabase(atom); + } else if (BytesCoder.isValidJSON(atom)) { + return BytesCoder.JSONToDatabase(atom); + } else { + return atom; + } +} + // Helper function to transform an atom from REST format to Mongo format. // An atom is anything that can't contain other expressions. So it // includes things where objects are used to represent other // datatypes, like pointers and dates, but it does not include objects // or arrays with generic stuff inside. -// If options.inArray is true, we'll leave it in REST format. -// If options.inObject is true, we'll leave files in REST format. // Raises an error if this cannot possibly be valid REST format. -// Returns CannotTransform if it's just not an atom, or if force is -// true, throws an error. -function transformAtom(atom, force, { - inArray, - inObject, -} = {}) { +// Returns CannotTransform if it's just not an atom +function transformTopLevelAtom(atom) { switch(typeof atom) { case 'string': case 'number': @@ -499,14 +512,7 @@ function transformAtom(atom, force, { // TODO: check validity harder for the __type-defined types if (atom.__type == 'Pointer') { - if (!inArray && !inObject) { - return `${atom.className}$${atom.objectId}`; - } - return { - __type: 'Pointer', - className: atom.className, - objectId: atom.objectId - }; + return `${atom.className}$${atom.objectId}`; } if (DateCoder.isValidJSON(atom)) { return DateCoder.JSONToDatabase(atom); @@ -515,17 +521,10 @@ function transformAtom(atom, force, { return BytesCoder.JSONToDatabase(atom); } if (GeoPointCoder.isValidJSON(atom)) { - return (inArray || inObject ? atom : GeoPointCoder.JSONToDatabase(atom)); + return GeoPointCoder.JSONToDatabase(atom); } if (FileCoder.isValidJSON(atom)) { - return (inArray || inObject ? atom : FileCoder.JSONToDatabase(atom)); - } - if (inArray || inObject) { - return atom; - } - - if (force) { - throw new Parse.Error(Parse.Error.INVALID_JSON, `bad atom: ${atom}`); + return FileCoder.JSONToDatabase(atom); } return CannotTransform; @@ -560,19 +559,24 @@ function transformConstraint(constraint, inArray) { case '$exists': case '$ne': case '$eq': - answer[key] = transformAtom(constraint[key], true, - {inArray: inArray}); + answer[key] = inArray ? transformInteriorAtom(constraint[key]) : transformTopLevelAtom(constraint[key]); + if (answer[key] === CannotTransform) { + throw new Parse.Error(Parse.Error.INVALID_JSON, `bad atom: ${atom}`); + } break; case '$in': case '$nin': var arr = constraint[key]; if (!(arr instanceof Array)) { - throw new Parse.Error(Parse.Error.INVALID_JSON, - 'bad ' + key + ' value'); + throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad ' + key + ' value'); } - answer[key] = arr.map((v) => { - return transformAtom(v, true, { inArray: inArray }); + answer[key] = arr.map(value => { + let result = inArray ? transformInteriorAtom(value) : transformTopLevelAtom(value); + if (result === CannotTransform) { + throw new Parse.Error(Parse.Error.INVALID_JSON, `bad atom: ${atom}`); + } + return result; }); break; @@ -582,9 +586,7 @@ function transformConstraint(constraint, inArray) { throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad ' + key + ' value'); } - answer[key] = arr.map((v) => { - return transformAtom(v, true, { inArray: true }); - }); + answer[key] = arr.map(transformInteriorAtom); break; case '$regex': @@ -667,14 +669,14 @@ function transformConstraint(constraint, inArray) { // The output for a non-flattened operator is a hash with __op being // the mongo op, and arg being the argument. // The output for a flattened operator is just a value. -// Returns CannotTransform if this cannot transform it. // Returns undefined if this should be a no-op. -function transformUpdateOperator(operator, flatten) { - if (typeof operator !== 'object' || !operator.__op) { - return CannotTransform; - } - switch(operator.__op) { +function transformUpdateOperator({ + __op, + amount, + objects, +}, flatten) { + switch(__op) { case 'Delete': if (flatten) { return undefined; @@ -683,43 +685,36 @@ function transformUpdateOperator(operator, flatten) { } case 'Increment': - if (typeof operator.amount !== 'number') { - throw new Parse.Error(Parse.Error.INVALID_JSON, - 'incrementing must provide a number'); + if (typeof amount !== 'number') { + throw new Parse.Error(Parse.Error.INVALID_JSON, 'incrementing must provide a number'); } if (flatten) { - return operator.amount; + return amount; } else { - return {__op: '$inc', arg: operator.amount}; + return {__op: '$inc', arg: amount}; } case 'Add': case 'AddUnique': - if (!(operator.objects instanceof Array)) { - throw new Parse.Error(Parse.Error.INVALID_JSON, - 'objects to add must be an array'); + if (!(objects instanceof Array)) { + throw new Parse.Error(Parse.Error.INVALID_JSON, 'objects to add must be an array'); } - var toAdd = operator.objects.map((obj) => { - return transformAtom(obj, true, { inArray: true }); - }); + var toAdd = objects.map(transformInteriorAtom); if (flatten) { return toAdd; } else { var mongoOp = { Add: '$push', AddUnique: '$addToSet' - }[operator.__op]; + }[__op]; return {__op: mongoOp, arg: {'$each': toAdd}}; } case 'Remove': - if (!(operator.objects instanceof Array)) { - throw new Parse.Error(Parse.Error.INVALID_JSON, - 'objects to remove must be an array'); + if (!(objects instanceof Array)) { + throw new Parse.Error(Parse.Error.INVALID_JSON, 'objects to remove must be an array'); } - var toRemove = operator.objects.map((obj) => { - return transformAtom(obj, true, { inArray: true }); - }); + var toRemove = objects.map(transformInteriorAtom); if (flatten) { return []; } else { @@ -727,9 +722,7 @@ function transformUpdateOperator(operator, flatten) { } default: - throw new Parse.Error( - Parse.Error.COMMAND_UNAVAILABLE, - 'the ' + operator.__op + ' op is not supported yet'); + throw new Parse.Error(Parse.Error.COMMAND_UNAVAILABLE, `The ${__op} operator is not supported yet.`); } } @@ -1037,7 +1030,7 @@ var FileCoder = { }; module.exports = { - transformKeyValue, + transformKey, parseObjectToMongoObjectForCreate, transformUpdate, transformWhere, diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index ab90fc3bfd..fefd5afe6b 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -616,64 +616,67 @@ DatabaseController.prototype.find = function(className, query, { let op = typeof query.objectId == 'string' && Object.keys(query).length === 1 ? 'get' : 'find'; return this.loadSchema() .then(schemaController => { - if (sort) { - mongoOptions.sort = {}; - for (let fieldName in sort) { - // Parse.com treats queries on _created_at and _updated_at as if they were queries on createdAt and updatedAt, - // so duplicate that behaviour here. - if (fieldName === '_created_at') { - fieldName = 'createdAt'; - sort['createdAt'] = sort['_created_at']; - } else if (fieldName === '_updated_at') { - fieldName = 'updatedAt'; - sort['updatedAt'] = sort['_updated_at']; - } + 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(schema => { + if (sort) { + mongoOptions.sort = {}; + for (let fieldName in sort) { + // Parse.com treats queries on _created_at and _updated_at as if they were queries on createdAt and updatedAt, + // so duplicate that behaviour here. + if (fieldName === '_created_at') { + fieldName = 'createdAt'; + sort['createdAt'] = sort['_created_at']; + } else if (fieldName === '_updated_at') { + fieldName = 'updatedAt'; + sort['updatedAt'] = sort['_updated_at']; + } - if (!SchemaController.fieldNameIsValid(fieldName)) { - throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name: ${fieldName}.`); + if (!SchemaController.fieldNameIsValid(fieldName)) { + throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name: ${fieldName}.`); + } + if (fieldName.match(/^authData\.([a-zA-Z0-9_]+)\.id$/)) { + throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Cannot sort by ${fieldName}`); + } + const mongoKey = this.transform.transformKey(className, fieldName, schema); + mongoOptions.sort[mongoKey] = sort[fieldName]; } - const mongoKey = this.transform.transformKeyValue(schemaController, className, fieldName, null).key; - mongoOptions.sort[mongoKey] = sort[fieldName]; } - } - 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 (!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); - } - 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: {} }; + if (!query) { + if (op == 'get') { + return Promise.reject(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, + 'Object not found.')); + } else { + return Promise.resolve([]); + } } - throw error; - }) - .then(parseFormatSchema => { - let mongoWhere = this.transform.transformWhere(className, query, {}, parseFormatSchema); + if (!isMaster) { + query = addReadACL(query, aclGroup); + } + let mongoWhere = this.transform.transformWhere(className, query, {}, schema); 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); + .then(mongoResults => { + return mongoResults.map(result => { + return this.untransformObject(schemaController, isMaster, aclGroup, className, result); }); }); } From 03108e634776ebeda71c0c3159f8fbedff1b796c Mon Sep 17 00:00:00 2001 From: Hussam Moqhim Date: Wed, 18 May 2016 22:06:37 -0500 Subject: [PATCH 41/48] add support for http basic auth (#1706) * add support for http basic auth * update http auth per flovilmart feedback --- spec/index.spec.js | 24 +++++++++++++++++++++++ src/middlewares.js | 47 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/spec/index.spec.js b/spec/index.spec.js index d0d54401b1..f76e809ed4 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -12,6 +12,30 @@ describe('server', () => { expect(setServerConfiguration.bind(undefined, { appId: 'myId', masterKey: 'mk' })).toThrow('You must provide a serverURL!'); done(); }); + + it('support http basic authentication with masterkey', done => { + request.get({ + url: 'http://localhost:8378/1/classes/TestObject', + headers: { + 'Authorization': 'Basic ' + new Buffer('test:' + 'test').toString('base64') + } + }, (error, response, body) => { + expect(response.statusCode).toEqual(200); + done(); + }); + }); + + it('support http basic authentication with javascriptKey', done => { + request.get({ + url: 'http://localhost:8378/1/classes/TestObject', + headers: { + 'Authorization': 'Basic ' + new Buffer('test:javascript-key=' + 'test').toString('base64') + } + }, (error, response, body) => { + expect(response.statusCode).toEqual(200); + done(); + }); + }); it('fails if database is unreachable', done => { setServerConfiguration({ diff --git a/src/middlewares.js b/src/middlewares.js index c500609363..d534215453 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -27,6 +27,14 @@ function handleParseHeaders(req, res, next) { dotNetKey: req.get('X-Parse-Windows-Key'), restAPIKey: req.get('X-Parse-REST-API-Key') }; + + var basicAuth = httpAuth(req); + + if (basicAuth) { + info.appId = basicAuth.appId + info.masterKey = basicAuth.masterKey || info.masterKey; + info.javascriptKey = basicAuth.javascriptKey || info.javascriptKey; + } if (req.body) { // Unity SDK sends a _noBody key which needs to be removed. @@ -144,6 +152,45 @@ function handleParseHeaders(req, res, next) { }); } +function httpAuth(req) { + if (!(req.req || req).headers.authorization) + return ; + + var header = (req.req || req).headers.authorization; + var appId, masterKey, javascriptKey; + + // parse header + var authPrefix = 'basic '; + + var match = header.toLowerCase().indexOf(authPrefix); + + if (match == 0) { + var encodedAuth = header.substring(authPrefix.length, header.length); + var credentials = decodeBase64(encodedAuth).split(':'); + + if (credentials.length == 2) { + appId = credentials[0]; + var key = credentials[1]; + + var jsKeyPrefix = 'javascript-key='; + + var matchKey = key.indexOf(jsKeyPrefix) + if (matchKey == 0) { + javascriptKey = key.substring(jsKeyPrefix.length, key.length); + } + else { + masterKey = key; + } + } + } + + return {appId: appId, masterKey: masterKey, javascriptKey: javascriptKey}; +} + +function decodeBase64(str) { + return new Buffer(str, 'base64').toString() +} + var allowCrossDomain = function(req, res, next) { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS'); From fece2a4b47f1469c6371c39bca63df85276e7d5f Mon Sep 17 00:00:00 2001 From: benishak Date: Thu, 19 May 2016 18:52:44 +0200 Subject: [PATCH 42/48] change logger.error to logger.info to prevent pm2 from crashing (#1830) --- src/pushStatusHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pushStatusHandler.js b/src/pushStatusHandler.js index dfe887f3ea..934e8fa27f 100644 --- a/src/pushStatusHandler.js +++ b/src/pushStatusHandler.js @@ -96,7 +96,7 @@ export default function pushStatusHandler(config) { status: 'failed', updatedAt: new Date() } - logger.error('error while sending push', err); + logger.info('warning: error while sending push', err); return database.update(PUSH_STATUS_COLLECTION, { objectId }, update); } From b2183680be18ffc6c5b90e2fc2260bede571c2b3 Mon Sep 17 00:00:00 2001 From: Drew Date: Thu, 19 May 2016 13:38:16 -0700 Subject: [PATCH 43/48] Refactor cloud code tests (#1837) * Move cloud code tests * Remove _removeHook calls that are no longer necessary * Use Strict --- spec/CloudCode.spec.js | 470 ++++++++++++++++++++++++++++ spec/CloudCodeLogger.spec.js | 2 - spec/ParseACL.spec.js | 1 - spec/ParseAPI.spec.js | 349 --------------------- spec/ParseRelation.spec.js | 4 - spec/ParseUser.spec.js | 8 - spec/cloud/cloudCodeAbsoluteFile.js | 3 + spec/cloud/cloudCodeRelativeFile.js | 3 + spec/cloud/main.js | 117 ------- spec/helper.js | 3 +- spec/index.spec.js | 24 +- src/cloud-code/Parse.Cloud.js | 8 +- src/triggers.js | 18 +- 13 files changed, 496 insertions(+), 514 deletions(-) create mode 100644 spec/CloudCode.spec.js create mode 100644 spec/cloud/cloudCodeAbsoluteFile.js create mode 100644 spec/cloud/cloudCodeRelativeFile.js delete mode 100644 spec/cloud/main.js diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js new file mode 100644 index 0000000000..2218a25706 --- /dev/null +++ b/spec/CloudCode.spec.js @@ -0,0 +1,470 @@ +"use strict" +const Parse = require("parse/node"); + +describe('Cloud Code', () => { + it('can load absolute cloud code file', done => { + setServerConfiguration({ + serverURL: 'http://localhost:8378/1', + appId: 'test', + masterKey: 'test', + cloud: __dirname + '/cloud/cloudCodeRelativeFile.js' + }); + Parse.Cloud.run('cloudCodeInFile', {}, result => { + expect(result).toEqual('It is possible to define cloud code in a file.'); + done(); + }); + }); + + it('can load relative cloud code file', done => { + setServerConfiguration({ + serverURL: 'http://localhost:8378/1', + appId: 'test', + masterKey: 'test', + cloud: './spec/cloud/cloudCodeAbsoluteFile.js' + }); + Parse.Cloud.run('cloudCodeInFile', {}, result => { + expect(result).toEqual('It is possible to define cloud code in a file.'); + done(); + }); + }); + + it('can create functions', done => { + Parse.Cloud.define('hello', (req, res) => { + res.success('Hello world!'); + }); + + Parse.Cloud.run('hello', {}, result => { + expect(result).toEqual('Hello world!'); + done(); + }); + }); + + it('is cleared cleared after the previous test', done => { + Parse.Cloud.run('hello', {}) + .catch(error => { + expect(error.code).toEqual(141); + done(); + }); + }); + + it('basic beforeSave rejection', function(done) { + Parse.Cloud.beforeSave('BeforeSaveFail', function(req, res) { + res.error('You shall not pass!'); + }); + + var obj = new Parse.Object('BeforeSaveFail'); + obj.set('foo', 'bar'); + obj.save().then(() => { + fail('Should not have been able to save BeforeSaveFailure class.'); + done(); + }, () => { + done(); + }) + }); + + it('basic beforeSave rejection via promise', function(done) { + Parse.Cloud.beforeSave('BeforeSaveFailWithPromise', function (req, res) { + var query = new Parse.Query('Yolo'); + query.find().then(() => { + res.error('Nope'); + }, () => { + res.success(); + }); + }); + + var obj = new Parse.Object('BeforeSaveFailWithPromise'); + obj.set('foo', 'bar'); + obj.save().then(function() { + fail('Should not have been able to save BeforeSaveFailure class.'); + done(); + }, function(error) { + expect(error.code).toEqual(Parse.Error.SCRIPT_FAILED); + expect(error.message).toEqual('Nope'); + done(); + }) + }); + + it('test beforeSave changed object success', function(done) { + Parse.Cloud.beforeSave('BeforeSaveChanged', function(req, res) { + req.object.set('foo', 'baz'); + res.success(); + }); + + var obj = new Parse.Object('BeforeSaveChanged'); + obj.set('foo', 'bar'); + obj.save().then(function() { + var query = new Parse.Query('BeforeSaveChanged'); + query.get(obj.id).then(function(objAgain) { + expect(objAgain.get('foo')).toEqual('baz'); + done(); + }, function(error) { + fail(error); + done(); + }); + }, function(error) { + fail(error); + done(); + }); + }); + + it('test beforeSave returns value on create and update', (done) => { + Parse.Cloud.beforeSave('BeforeSaveChanged', function(req, res) { + req.object.set('foo', 'baz'); + res.success(); + }); + + var obj = new Parse.Object('BeforeSaveChanged'); + obj.set('foo', 'bing'); + obj.save().then(() => { + expect(obj.get('foo')).toEqual('baz'); + obj.set('foo', 'bar'); + return obj.save().then(() => { + expect(obj.get('foo')).toEqual('baz'); + done(); + }) + }) + }); + + it('test afterSave ran and created an object', function(done) { + Parse.Cloud.afterSave('AfterSaveTest', function(req) { + var obj = new Parse.Object('AfterSaveProof'); + obj.set('proof', req.object.id); + obj.save(); + }); + + var obj = new Parse.Object('AfterSaveTest'); + obj.save(); + + setTimeout(function() { + var query = new Parse.Query('AfterSaveProof'); + query.equalTo('proof', obj.id); + query.find().then(function(results) { + expect(results.length).toEqual(1); + done(); + }, function(error) { + fail(error); + done(); + }); + }, 500); + }); + + it('test beforeSave happens on update', function(done) { + Parse.Cloud.beforeSave('BeforeSaveChanged', function(req, res) { + req.object.set('foo', 'baz'); + res.success(); + }); + + var obj = new Parse.Object('BeforeSaveChanged'); + obj.set('foo', 'bar'); + obj.save().then(function() { + obj.set('foo', 'bar'); + return obj.save(); + }).then(function() { + var query = new Parse.Query('BeforeSaveChanged'); + return query.get(obj.id).then(function(objAgain) { + expect(objAgain.get('foo')).toEqual('baz'); + done(); + }); + }, function(error) { + fail(error); + done(); + }); + }); + + it('test beforeDelete failure', function(done) { + Parse.Cloud.beforeDelete('BeforeDeleteFail', function(req, res) { + res.error('Nope'); + }); + + var obj = new Parse.Object('BeforeDeleteFail'); + var id; + obj.set('foo', 'bar'); + obj.save().then(() => { + id = obj.id; + return obj.destroy(); + }).then(() => { + fail('obj.destroy() should have failed, but it succeeded'); + done(); + }, (error) => { + expect(error.code).toEqual(Parse.Error.SCRIPT_FAILED); + expect(error.message).toEqual('Nope'); + + var objAgain = new Parse.Object('BeforeDeleteFail', {objectId: id}); + return objAgain.fetch(); + }).then((objAgain) => { + if (objAgain) { + expect(objAgain.get('foo')).toEqual('bar'); + } else { + fail("unable to fetch the object ", id); + } + done(); + }, (error) => { + // We should have been able to fetch the object again + fail(error); + }); + }); + + it('basic beforeDelete rejection via promise', function(done) { + Parse.Cloud.beforeSave('BeforeDeleteFailWithPromise', function (req, res) { + var query = new Parse.Query('Yolo'); + query.find().then(() => { + res.error('Nope'); + }, () => { + res.success(); + }); + }); + + var obj = new Parse.Object('BeforeDeleteFailWithPromise'); + obj.set('foo', 'bar'); + obj.save().then(function() { + fail('Should not have been able to save BeforeSaveFailure class.'); + done(); + }, function(error) { + expect(error.code).toEqual(Parse.Error.SCRIPT_FAILED); + expect(error.message).toEqual('Nope'); + + done(); + }) + }); + + it('test afterDelete ran and created an object', function(done) { + Parse.Cloud.afterDelete('AfterDeleteTest', function(req) { + var obj = new Parse.Object('AfterDeleteProof'); + obj.set('proof', req.object.id); + obj.save(); + }); + + var obj = new Parse.Object('AfterDeleteTest'); + obj.save().then(function() { + obj.destroy(); + }); + + setTimeout(function() { + var query = new Parse.Query('AfterDeleteProof'); + query.equalTo('proof', obj.id); + query.find().then(function(results) { + expect(results.length).toEqual(1); + done(); + }, function(error) { + fail(error); + done(); + }); + }, 500); + }); + + it('test cloud function return types', function(done) { + Parse.Cloud.define('foo', function(req, res) { + res.success({ + object: { + __type: 'Object', + className: 'Foo', + objectId: '123', + x: 2, + relation: { + __type: 'Object', + className: 'Bar', + objectId: '234', + x: 3 + } + }, + array: [{ + __type: 'Object', + className: 'Bar', + objectId: '345', + x: 2 + }], + a: 2 + }); + }); + + Parse.Cloud.run('foo').then((result) => { + expect(result.object instanceof Parse.Object).toBeTruthy(); + if (!result.object) { + fail("Unable to run foo"); + done(); + return; + } + expect(result.object.className).toEqual('Foo'); + expect(result.object.get('x')).toEqual(2); + var bar = result.object.get('relation'); + expect(bar instanceof Parse.Object).toBeTruthy(); + expect(bar.className).toEqual('Bar'); + expect(bar.get('x')).toEqual(3); + expect(Array.isArray(result.array)).toEqual(true); + expect(result.array[0] instanceof Parse.Object).toBeTruthy(); + expect(result.array[0].get('x')).toEqual(2); + done(); + }); + }); + + it('test cloud function should echo keys', function(done) { + Parse.Cloud.define('echoKeys', function(req, res){ + return res.success({ + applicationId: Parse.applicationId, + masterKey: Parse.masterKey, + javascriptKey: Parse.javascriptKey + }) + }); + + Parse.Cloud.run('echoKeys').then((result) => { + expect(result.applicationId).toEqual(Parse.applicationId); + expect(result.masterKey).toEqual(Parse.masterKey); + expect(result.javascriptKey).toEqual(Parse.javascriptKey); + done(); + }); + }); + + it('should properly create an object in before save', done => { + Parse.Cloud.beforeSave('BeforeSaveChanged', function(req, res) { + req.object.set('foo', 'baz'); + res.success(); + }); + + Parse.Cloud.define('createBeforeSaveChangedObject', function(req, res){ + var obj = new Parse.Object('BeforeSaveChanged'); + obj.save().then(() => { + res.success(obj); + }) + }) + + Parse.Cloud.run('createBeforeSaveChangedObject').then((res) => { + expect(res.get('foo')).toEqual('baz'); + done(); + }); + }); + + it('dirtyKeys are set on update', done => { + let triggerTime = 0; + // Register a mock beforeSave hook + Parse.Cloud.beforeSave('GameScore', (req, res) => { + var object = req.object; + expect(object instanceof Parse.Object).toBeTruthy(); + expect(object.get('fooAgain')).toEqual('barAgain'); + if (triggerTime == 0) { + // Create + expect(object.get('foo')).toEqual('bar'); + } else if (triggerTime == 1) { + // Update + expect(object.dirtyKeys()).toEqual(['foo']); + expect(object.dirty('foo')).toBeTruthy(); + expect(object.get('foo')).toEqual('baz'); + } else { + res.error(); + } + triggerTime++; + res.success(); + }); + + let obj = new Parse.Object('GameScore'); + obj.set('foo', 'bar'); + obj.set('fooAgain', 'barAgain'); + obj.save().then(() => { + // We only update foo + obj.set('foo', 'baz'); + return obj.save(); + }).then(() => { + // Make sure the checking has been triggered + expect(triggerTime).toBe(2); + done(); + }, function(error) { + fail(error); + done(); + }); + }); + + it('test beforeSave unchanged success', function(done) { + Parse.Cloud.beforeSave('BeforeSaveUnchanged', function(req, res) { + res.success(); + }); + + var obj = new Parse.Object('BeforeSaveUnchanged'); + obj.set('foo', 'bar'); + obj.save().then(function() { + done(); + }, function(error) { + fail(error); + done(); + }); + }); + + it('test beforeDelete success', function(done) { + Parse.Cloud.beforeDelete('BeforeDeleteTest', function(req, res) { + res.success(); + }); + + var obj = new Parse.Object('BeforeDeleteTest'); + obj.set('foo', 'bar'); + obj.save().then(function() { + return obj.destroy(); + }).then(function() { + var objAgain = new Parse.Object('BeforeDeleteTest', obj.id); + return objAgain.fetch().then(fail, done); + }, function(error) { + fail(error); + done(); + }); + }); + + it('test save triggers get user', function(done) { + Parse.Cloud.beforeSave('SaveTriggerUser', function(req, res) { + if (req.user && req.user.id) { + res.success(); + } else { + res.error('No user present on request object for beforeSave.'); + } + }); + + Parse.Cloud.afterSave('SaveTriggerUser', function(req) { + if (!req.user || !req.user.id) { + console.log('No user present on request object for afterSave.'); + } + }); + + var user = new Parse.User(); + user.set("password", "asdf"); + user.set("email", "asdf@example.com"); + user.set("username", "zxcv"); + user.signUp(null, { + success: function() { + var obj = new Parse.Object('SaveTriggerUser'); + obj.save().then(function() { + done(); + }, function(error) { + fail(error); + done(); + }); + } + }); + }); + + it('beforeSave change propagates through the save response', (done) => { + Parse.Cloud.beforeSave('ChangingObject', function(request, response) { + request.object.set('foo', 'baz'); + response.success(); + }); + let obj = new Parse.Object('ChangingObject'); + obj.save({ foo: 'bar' }).then((objAgain) => { + expect(objAgain.get('foo')).toEqual('baz'); + done(); + }, (e) => { + fail('Should not have failed to save.'); + done(); + }); + }); + + it('test cloud function parameter validation success', (done) => { + // Register a function with validation + Parse.Cloud.define('functionWithParameterValidation', (req, res) => { + res.success('works'); + }, (request) => { + return request.params.success === 100; + }); + + Parse.Cloud.run('functionWithParameterValidation', {"success":100}).then((s) => { + done(); + }, (e) => { + fail('Validation should not have failed.'); + done(); + }); + }); +}); diff --git a/spec/CloudCodeLogger.spec.js b/spec/CloudCodeLogger.spec.js index 23fc967e0c..a556762a8f 100644 --- a/spec/CloudCodeLogger.spec.js +++ b/spec/CloudCodeLogger.spec.js @@ -14,7 +14,6 @@ describe("Cloud Code Logger", () => { }); Parse.Cloud.run('loggerTest').then(() => { - Parse.Cloud._removeHook('Functions', 'logTest'); return logController.getLogs({from: Date.now() - 500, size: 1000}); }).then((res) => { expect(res.length).not.toBe(0); @@ -42,7 +41,6 @@ describe("Cloud Code Logger", () => { let obj = new Parse.Object('MyObject'); obj.save().then(() => { - Parse.Cloud._removeHook('Triggers', 'beforeSave', 'MyObject'); return logController.getLogs({from: Date.now() - 500, size: 1000}) }).then((res) => { expect(res.length).not.toBe(0); diff --git a/spec/ParseACL.spec.js b/spec/ParseACL.spec.js index 79364b0ce4..ac25793b8f 100644 --- a/spec/ParseACL.spec.js +++ b/spec/ParseACL.spec.js @@ -1182,7 +1182,6 @@ describe('Parse.ACL', () => { done(); }, error => { expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); - Parse.Cloud._removeHook('Triggers', 'afterSave', Parse.User.className); done(); }); }); diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 5f44dca79f..21603a0956 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -164,67 +164,6 @@ describe('miscellaneous', function() { }); }); - it('test cloud function', function(done) { - Parse.Cloud.run('hello', {}, function(result) { - expect(result).toEqual('Hello world!'); - done(); - }); - }); - - it('basic beforeSave rejection', function(done) { - var obj = new Parse.Object('BeforeSaveFail'); - obj.set('foo', 'bar'); - obj.save().then(() => { - fail('Should not have been able to save BeforeSaveFailure class.'); - done(); - }, () => { - done(); - }) - }); - - it('basic beforeSave rejection via promise', function(done) { - var obj = new Parse.Object('BeforeSaveFailWithPromise'); - obj.set('foo', 'bar'); - obj.save().then(function() { - fail('Should not have been able to save BeforeSaveFailure class.'); - done(); - }, function(error) { - expect(error.code).toEqual(Parse.Error.SCRIPT_FAILED); - expect(error.message).toEqual('Nope'); - - done(); - }) - }); - - it('test beforeSave unchanged success', function(done) { - var obj = new Parse.Object('BeforeSaveUnchanged'); - obj.set('foo', 'bar'); - obj.save().then(function() { - done(); - }, function(error) { - fail(error); - done(); - }); - }); - - it('test beforeSave changed object success', function(done) { - var obj = new Parse.Object('BeforeSaveChanged'); - obj.set('foo', 'bar'); - obj.save().then(function() { - var query = new Parse.Query('BeforeSaveChanged'); - query.get(obj.id).then(function(objAgain) { - expect(objAgain.get('foo')).toEqual('baz'); - done(); - }, function(error) { - fail(error); - done(); - }); - }, function(error) { - fail(error); - done(); - }); - }); - it('test beforeSave set object acl success', function(done) { var acl = new Parse.ACL({ '*': { read: true, write: false } @@ -237,7 +176,6 @@ describe('miscellaneous', function() { var obj = new Parse.Object('BeforeSaveAddACL'); obj.set('lol', true); obj.save().then(function() { - Parse.Cloud._removeHook('Triggers', 'beforeSave', 'BeforeSaveAddACL'); var query = new Parse.Query('BeforeSaveAddACL'); query.get(obj.id).then(function(objAgain) { expect(objAgain.get('lol')).toBeTruthy(); @@ -253,185 +191,6 @@ describe('miscellaneous', function() { }); }); - it('test beforeSave returns value on create and update', (done) => { - var obj = new Parse.Object('BeforeSaveChanged'); - obj.set('foo', 'bing'); - obj.save().then(() => { - expect(obj.get('foo')).toEqual('baz'); - obj.set('foo', 'bar'); - return obj.save().then(() => { - expect(obj.get('foo')).toEqual('baz'); - done(); - }) - }) - }); - - it('test afterSave ran and created an object', function(done) { - var obj = new Parse.Object('AfterSaveTest'); - obj.save(); - - setTimeout(function() { - var query = new Parse.Query('AfterSaveProof'); - query.equalTo('proof', obj.id); - query.find().then(function(results) { - expect(results.length).toEqual(1); - done(); - }, function(error) { - fail(error); - done(); - }); - }, 500); - }); - - it('test beforeSave happens on update', function(done) { - var obj = new Parse.Object('BeforeSaveChanged'); - obj.set('foo', 'bar'); - obj.save().then(function() { - obj.set('foo', 'bar'); - return obj.save(); - }).then(function() { - var query = new Parse.Query('BeforeSaveChanged'); - return query.get(obj.id).then(function(objAgain) { - expect(objAgain.get('foo')).toEqual('baz'); - done(); - }); - }, function(error) { - fail(error); - done(); - }); - }); - - it('test beforeDelete failure', function(done) { - var obj = new Parse.Object('BeforeDeleteFail'); - var id; - obj.set('foo', 'bar'); - obj.save().then(() => { - id = obj.id; - return obj.destroy(); - }).then(() => { - fail('obj.destroy() should have failed, but it succeeded'); - done(); - }, (error) => { - expect(error.code).toEqual(Parse.Error.SCRIPT_FAILED); - expect(error.message).toEqual('Nope'); - - var objAgain = new Parse.Object('BeforeDeleteFail', {objectId: id}); - return objAgain.fetch(); - }).then((objAgain) => { - if (objAgain) { - expect(objAgain.get('foo')).toEqual('bar'); - } else { - fail("unable to fetch the object ", id); - } - done(); - }, (error) => { - // We should have been able to fetch the object again - fail(error); - }); - }); - - it('basic beforeDelete rejection via promise', function(done) { - var obj = new Parse.Object('BeforeDeleteFailWithPromise'); - obj.set('foo', 'bar'); - obj.save().then(function() { - fail('Should not have been able to save BeforeSaveFailure class.'); - done(); - }, function(error) { - expect(error.code).toEqual(Parse.Error.SCRIPT_FAILED); - expect(error.message).toEqual('Nope'); - - done(); - }) - }); - - it('test beforeDelete success', function(done) { - var obj = new Parse.Object('BeforeDeleteTest'); - obj.set('foo', 'bar'); - obj.save().then(function() { - return obj.destroy(); - }).then(function() { - var objAgain = new Parse.Object('BeforeDeleteTest', obj.id); - return objAgain.fetch().then(fail, done); - }, function(error) { - fail(error); - done(); - }); - }); - - it('test afterDelete ran and created an object', function(done) { - var obj = new Parse.Object('AfterDeleteTest'); - obj.save().then(function() { - obj.destroy(); - }); - - setTimeout(function() { - var query = new Parse.Query('AfterDeleteProof'); - query.equalTo('proof', obj.id); - query.find().then(function(results) { - expect(results.length).toEqual(1); - done(); - }, function(error) { - fail(error); - done(); - }); - }, 500); - }); - - it('test save triggers get user', function(done) { - var user = new Parse.User(); - user.set("password", "asdf"); - user.set("email", "asdf@example.com"); - user.set("username", "zxcv"); - user.signUp(null, { - success: function() { - var obj = new Parse.Object('SaveTriggerUser'); - obj.save().then(function() { - done(); - }, function(error) { - fail(error); - done(); - }); - } - }); - }); - - it('test cloud function return types', function(done) { - Parse.Cloud.run('foo').then((result) => { - expect(result.object instanceof Parse.Object).toBeTruthy(); - if (!result.object) { - fail("Unable to run foo"); - done(); - return; - } - expect(result.object.className).toEqual('Foo'); - expect(result.object.get('x')).toEqual(2); - var bar = result.object.get('relation'); - expect(bar instanceof Parse.Object).toBeTruthy(); - expect(bar.className).toEqual('Bar'); - expect(bar.get('x')).toEqual(3); - expect(Array.isArray(result.array)).toEqual(true); - expect(result.array[0] instanceof Parse.Object).toBeTruthy(); - expect(result.array[0].get('x')).toEqual(2); - done(); - }); - }); - - it('test cloud function should echo keys', function(done) { - Parse.Cloud.run('echoKeys').then((result) => { - expect(result.applicationId).toEqual(Parse.applicationId); - expect(result.masterKey).toEqual(Parse.masterKey); - expect(result.javascriptKey).toEqual(Parse.javascriptKey); - done(); - }); - }); - - it('should properly create an object in before save', (done) => { - Parse.Cloud.run('createBeforeSaveChangedObject').then((res) => { - expect(res.get('foo')).toEqual('baz'); - done(); - }); - }) - it('test rest_create_app', function(done) { var appId; Parse._request('POST', 'rest_create_app').then((res) => { @@ -456,17 +215,6 @@ describe('miscellaneous', function() { }); describe('beforeSave', () => { - beforeEach(done => { - // Make sure the required mock for all tests is unset. - Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore"); - done(); - }); - afterEach(done => { - // Make sure the required mock for all tests is unset. - Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore"); - done(); - }); - it('object is set on create and update', done => { let triggerTime = 0; // Register a mock beforeSave hook @@ -511,45 +259,6 @@ describe('miscellaneous', function() { }); }); - it('dirtyKeys are set on update', done => { - let triggerTime = 0; - // Register a mock beforeSave hook - Parse.Cloud.beforeSave('GameScore', (req, res) => { - var object = req.object; - expect(object instanceof Parse.Object).toBeTruthy(); - expect(object.get('fooAgain')).toEqual('barAgain'); - if (triggerTime == 0) { - // Create - expect(object.get('foo')).toEqual('bar'); - } else if (triggerTime == 1) { - // Update - expect(object.dirtyKeys()).toEqual(['foo']); - expect(object.dirty('foo')).toBeTruthy(); - expect(object.get('foo')).toEqual('baz'); - } else { - res.error(); - } - triggerTime++; - res.success(); - }); - - let obj = new Parse.Object('GameScore'); - obj.set('foo', 'bar'); - obj.set('fooAgain', 'barAgain'); - obj.save().then(() => { - // We only update foo - obj.set('foo', 'baz'); - return obj.save(); - }).then(() => { - // Make sure the checking has been triggered - expect(triggerTime).toBe(2); - done(); - }, function(error) { - fail(error); - done(); - }); - }); - it('original object is set on update', done => { let triggerTime = 0; // Register a mock beforeSave hook @@ -673,7 +382,6 @@ describe('miscellaneous', function() { return obj.save(); }).then((obj) => { expect(obj.get('point').id).toEqual(pointId); - Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore"); done(); }) }); @@ -711,8 +419,6 @@ describe('miscellaneous', function() { }).then(function() { // Make sure the checking has been triggered expect(triggerTime).toBe(2); - // Clear mock beforeSave - Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore"); done(); }, function(error) { fail(error); @@ -764,8 +470,6 @@ describe('miscellaneous', function() { }).then(function() { // Make sure the checking has been triggered expect(triggerTime).toBe(2); - // Clear mock afterSave - Parse.Cloud._removeHook("Triggers", "afterSave", "GameScore"); done(); }, function(error) { console.error(error); @@ -814,8 +518,6 @@ describe('miscellaneous', function() { }).then(function() { // Make sure the checking has been triggered expect(triggerTime).toBe(2); - // Clear mock afterSave - Parse.Cloud._removeHook("Triggers", "afterSave", "GameScore"); done(); }, function(error) { console.error(error); @@ -854,8 +556,6 @@ describe('miscellaneous', function() { }).then(() => { // Make sure the checking has been triggered expect(triggerTime).toBe(2); - // Clear mock afterSave - Parse.Cloud._removeHook("Triggers", "afterSave", "GameScore"); done(); }, error => { console.error(error); @@ -896,8 +596,6 @@ describe('miscellaneous', function() { }).then(() => { // Make sure the checking has been triggered expect(triggerTime).toBe(2); - // Clear mock afterSave - Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore"); done(); }, error => { console.error(error); @@ -938,8 +636,6 @@ describe('miscellaneous', function() { }).then(() => { // Make sure the checking has been triggered expect(triggerTime).toBe(2); - // Clear mock afterSave - Parse.Cloud._removeHook("Triggers", "afterSave", "GameScore"); done(); }, error => { console.error(error); @@ -999,12 +695,10 @@ describe('miscellaneous', function() { }); Parse.Cloud.run('willFail').then((s) => { fail('Should not have succeeded.'); - Parse.Cloud._removeHook("Functions", "willFail"); done(); }, (e) => { expect(e.code).toEqual(141); expect(e.message).toEqual('noway'); - Parse.Cloud._removeHook("Functions", "willFail"); done(); }); }); @@ -1036,9 +730,6 @@ describe('miscellaneous', function() { }, (error, response, body) => { expect(error).toBe(null); expect(triggerTime).toEqual(2); - - Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore"); - Parse.Cloud._removeHook("Triggers", "afterSave", "GameScore"); done(); }); }); @@ -1075,9 +766,6 @@ describe('miscellaneous', function() { }, (error, response, body) => { expect(error).toBe(null); expect(triggerTime).toEqual(2); - - Parse.Cloud._removeHook("Triggers", "beforeDelete", "GameScore"); - Parse.Cloud._removeHook("Triggers", "afterDelete", "GameScore"); done(); }); }); @@ -1107,24 +795,6 @@ describe('miscellaneous', function() { // Make sure query string params override body params expect(res.other).toEqual('2'); expect(res.foo).toEqual("bar"); - Parse.Cloud._removeHook("Functions",'echoParams'); - done(); - }); - }); - - it('test cloud function parameter validation success', (done) => { - // Register a function with validation - Parse.Cloud.define('functionWithParameterValidation', (req, res) => { - res.success('works'); - }, (request) => { - return request.params.success === 100; - }); - - Parse.Cloud.run('functionWithParameterValidation', {"success":100}).then((s) => { - Parse.Cloud._removeHook("Functions", "functionWithParameterValidation"); - done(); - }, (e) => { - fail('Validation should not have failed.'); done(); }); }); @@ -1139,7 +809,6 @@ describe('miscellaneous', function() { Parse.Cloud.run('functionWithParameterValidationFailure', {"success":500}).then((s) => { fail('Validation should not have succeeded'); - Parse.Cloud._removeHook("Functions", "functionWithParameterValidationFailure"); done(); }, (e) => { expect(e.code).toEqual(141); @@ -1156,7 +825,6 @@ describe('miscellaneous', function() { Parse.Cloud.run('func', {nullParam: null}) .then(() => { - Parse.Cloud._removeHook('Functions', 'func'); done() }, e => { fail('cloud code call failed'); @@ -1243,23 +911,6 @@ describe('miscellaneous', function() { }); }); - it('beforeSave change propagates through the save response', (done) => { - Parse.Cloud.beforeSave('ChangingObject', function(request, response) { - request.object.set('foo', 'baz'); - response.success(); - }); - let obj = new Parse.Object('ChangingObject'); - obj.save({ foo: 'bar' }).then((objAgain) => { - expect(objAgain.get('foo')).toEqual('baz'); - Parse.Cloud._removeHook("Triggers", "beforeSave", "ChangingObject"); - done(); - }, (e) => { - Parse.Cloud._removeHook("Triggers", "beforeSave", "ChangingObject"); - fail('Should not have failed to save.'); - done(); - }); - }); - it('dedupes an installation properly and returns updatedAt', (done) => { let headers = { 'Content-Type': 'application/json', diff --git a/spec/ParseRelation.spec.js b/spec/ParseRelation.spec.js index 8ff6c6c37b..6b79743bb3 100644 --- a/spec/ParseRelation.spec.js +++ b/spec/ParseRelation.spec.js @@ -696,20 +696,16 @@ describe('Parse.Relation testing', () => { admins.first({ useMasterKey: true }) .then(user => { if (user) { - Parse.Cloud._removeHook('Functions', 'isAdmin'); done(); } else { - Parse.Cloud._removeHook('Functions', 'isAdmin'); fail('Should have found admin user, found nothing instead'); done(); } }, error => { - Parse.Cloud._removeHook('Functions', 'isAdmin'); fail('User not admin'); done(); }) }, error => { - Parse.Cloud._removeHook('Functions', 'isAdmin'); fail('Should have found admin user, errored instead'); fail(error); done(); diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index ce9763af89..f333a714e1 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -1156,12 +1156,10 @@ describe('Parse.User testing', () => { Parse.User._logInWith("facebook", { success: function(innerModel) { - Parse.Cloud._removeHook('Triggers', 'beforeSave', Parse.User.className); done(); }, error: function(model, error) { ok(undefined, error); - Parse.Cloud._removeHook('Triggers', 'beforeSave', Parse.User.className); done(); } }); @@ -1584,8 +1582,6 @@ describe('Parse.User testing', () => { Parse.User._registerAuthenticationProvider(provider); Parse.User._logInWith("facebook", { success: function(model) { - Parse.Cloud._removeHook('Triggers', 'beforeSave', Parse.User.className); - Parse.Cloud._removeHook('Triggers', 'afterSave', Parse.User.className); done(); } }); @@ -2213,11 +2209,9 @@ describe('Parse.User testing', () => { }).then((user) => { expect(typeof user).toEqual('object'); expect(user.authData).toBeUndefined(); - Parse.Cloud._removeHook('Triggers', 'beforeSave', '_User'); done(); }).catch((err) => { fail('no request should fail: ' + JSON.stringify(err)); - Parse.Cloud._removeHook('Triggers', 'beforeSave', '_User'); done(); }); }); @@ -2237,7 +2231,6 @@ describe('Parse.User testing', () => { user.set('hello', 'world'); return user.save(); }).then(() => { - Parse.Cloud._removeHook('Triggers', 'afterSave', '_User'); done(); }); }); @@ -2395,7 +2388,6 @@ describe('Parse.User testing', () => { serverURL: 'http://localhost:8378/1', appId: 'test', masterKey: 'test', - cloud: './spec/cloud/main.js', revokeSessionOnPasswordReset: false, }) request.post({ diff --git a/spec/cloud/cloudCodeAbsoluteFile.js b/spec/cloud/cloudCodeAbsoluteFile.js new file mode 100644 index 0000000000..f5fcf2b856 --- /dev/null +++ b/spec/cloud/cloudCodeAbsoluteFile.js @@ -0,0 +1,3 @@ +Parse.Cloud.define('cloudCodeInFile', (req, res) => { + res.success('It is possible to define cloud code in a file.'); +}); diff --git a/spec/cloud/cloudCodeRelativeFile.js b/spec/cloud/cloudCodeRelativeFile.js new file mode 100644 index 0000000000..f5fcf2b856 --- /dev/null +++ b/spec/cloud/cloudCodeRelativeFile.js @@ -0,0 +1,3 @@ +Parse.Cloud.define('cloudCodeInFile', (req, res) => { + res.success('It is possible to define cloud code in a file.'); +}); diff --git a/spec/cloud/main.js b/spec/cloud/main.js deleted file mode 100644 index 0785c0a624..0000000000 --- a/spec/cloud/main.js +++ /dev/null @@ -1,117 +0,0 @@ -Parse.Cloud.define('hello', function(req, res) { - res.success('Hello world!'); -}); - -Parse.Cloud.beforeSave('BeforeSaveFail', function(req, res) { - res.error('You shall not pass!'); -}); - -Parse.Cloud.beforeSave('BeforeSaveFailWithPromise', function (req, res) { - var query = new Parse.Query('Yolo'); - query.find().then(() => { - res.error('Nope'); - }, () => { - res.success(); - }); -}); - -Parse.Cloud.beforeSave('BeforeSaveUnchanged', function(req, res) { - res.success(); -}); - -Parse.Cloud.beforeSave('BeforeSaveChanged', function(req, res) { - req.object.set('foo', 'baz'); - res.success(); -}); - -Parse.Cloud.afterSave('AfterSaveTest', function(req) { - var obj = new Parse.Object('AfterSaveProof'); - obj.set('proof', req.object.id); - obj.save(); -}); - -Parse.Cloud.beforeDelete('BeforeDeleteFail', function(req, res) { - res.error('Nope'); -}); - -Parse.Cloud.beforeSave('BeforeDeleteFailWithPromise', function (req, res) { - var query = new Parse.Query('Yolo'); - query.find().then(() => { - res.error('Nope'); - }, () => { - res.success(); - }); -}); - -Parse.Cloud.beforeDelete('BeforeDeleteTest', function(req, res) { - res.success(); -}); - -Parse.Cloud.afterDelete('AfterDeleteTest', function(req) { - var obj = new Parse.Object('AfterDeleteProof'); - obj.set('proof', req.object.id); - obj.save(); -}); - -Parse.Cloud.beforeSave('SaveTriggerUser', function(req, res) { - if (req.user && req.user.id) { - res.success(); - } else { - res.error('No user present on request object for beforeSave.'); - } -}); - -Parse.Cloud.afterSave('SaveTriggerUser', function(req) { - if (!req.user || !req.user.id) { - console.log('No user present on request object for afterSave.'); - } -}); - -Parse.Cloud.define('foo', function(req, res) { - res.success({ - object: { - __type: 'Object', - className: 'Foo', - objectId: '123', - x: 2, - relation: { - __type: 'Object', - className: 'Bar', - objectId: '234', - x: 3 - } - }, - array: [{ - __type: 'Object', - className: 'Bar', - objectId: '345', - x: 2 - }], - a: 2 - }); -}); - -Parse.Cloud.define('bar', function(req, res) { - res.error('baz'); -}); - -Parse.Cloud.define('requiredParameterCheck', function(req, res) { - res.success(); -}, function(params) { - return params.name; -}); - -Parse.Cloud.define('echoKeys', function(req, res){ - return res.success({ - applicationId: Parse.applicationId, - masterKey: Parse.masterKey, - javascriptKey: Parse.javascriptKey - }) -}); - -Parse.Cloud.define('createBeforeSaveChangedObject', function(req, res){ - var obj = new Parse.Object('BeforeSaveChanged'); - obj.save().then(() => { - res.success(obj); - }) -}) diff --git a/spec/helper.js b/spec/helper.js index aa63ff0f4d..049c9664fa 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -12,13 +12,11 @@ var TestUtils = require('../src/index').TestUtils; var MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter'); var databaseURI = process.env.DATABASE_URI; -var cloudMain = process.env.CLOUD_CODE_MAIN || './spec/cloud/main.js'; var port = 8378; // Default server configuration for tests. var defaultConfiguration = { databaseURI: databaseURI, - cloud: cloudMain, serverURL: 'http://localhost:' + port + '/1', appId: 'test', javascriptKey: 'test', @@ -94,6 +92,7 @@ var mongoAdapter = new MongoStorageAdapter({ }) afterEach(function(done) { + Parse.Cloud._removeAllHooks(); mongoAdapter.getAllSchemas() .then(allSchemas => { allSchemas.forEach((schema) => { diff --git a/spec/index.spec.js b/spec/index.spec.js index f76e809ed4..6ea648423a 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -12,7 +12,7 @@ describe('server', () => { expect(setServerConfiguration.bind(undefined, { appId: 'myId', masterKey: 'mk' })).toThrow('You must provide a serverURL!'); done(); }); - + it('support http basic authentication with masterkey', done => { request.get({ url: 'http://localhost:8378/1/classes/TestObject', @@ -24,7 +24,7 @@ describe('server', () => { done(); }); }); - + it('support http basic authentication with javascriptKey', done => { request.get({ url: 'http://localhost:8378/1/classes/TestObject', @@ -199,26 +199,6 @@ describe('server', () => { }) }); - it('can load absolute cloud code file', done => { - setServerConfiguration({ - serverURL: 'http://localhost:8378/1', - appId: 'test', - masterKey: 'test', - cloud: __dirname + '/cloud/main.js' - }); - done(); - }); - - it('can load relative cloud code file', done => { - setServerConfiguration({ - serverURL: 'http://localhost:8378/1', - appId: 'test', - masterKey: 'test', - cloud: './spec/cloud/main.js' - }); - done(); - }); - it('can create a parse-server', done => { var parseServer = new ParseServer.default({ appId: "aTestApp", diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index e1b9ec3ab2..ca323f0ac5 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -1,4 +1,4 @@ -import { Parse } from 'parse/node'; +import { Parse } from 'parse/node'; import * as triggers from '../triggers'; function validateClassNameForTriggers(className) { @@ -40,12 +40,16 @@ ParseCloud.afterDelete = function(parseClass, handler) { var className = getClassName(parseClass); triggers.addTrigger(triggers.Types.afterDelete, className, handler, Parse.applicationId); }; - + ParseCloud._removeHook = function(category, name, type, applicationId) { applicationId = applicationId || Parse.applicationId; triggers._unregister(applicationId, category, name, type); }; +ParseCloud._removeAllHooks = () => { + triggers._unregisterAll(); +} + ParseCloud.httpRequest = require("./httpRequest"); module.exports = ParseCloud; diff --git a/src/triggers.js b/src/triggers.js index c7c9ba2c1c..0827de7a13 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -1,6 +1,6 @@ // triggers.js -import Parse from 'parse/node'; -import AppCache from './cache'; +import Parse from 'parse/node'; +import AppCache from './cache'; export const Types = { beforeSave: 'beforeSave', @@ -49,15 +49,19 @@ export function removeTrigger(type, className, applicationId) { delete _triggerStore[applicationId].Triggers[type][className] } -export function _unregister(a,b,c,d) { - if (d) { - removeTrigger(c,d,a); - delete _triggerStore[a][b][c][d]; +export function _unregister(appId,category,className,type) { + if (type) { + removeTrigger(className,type,appId); + delete _triggerStore[appId][category][className][type]; } else { - delete _triggerStore[a][b][c]; + delete _triggerStore[appId][category][className]; } } +export function _unregisterAll() { + Object.keys(_triggerStore).forEach(appId => delete _triggerStore[appId]); +} + export function getTrigger(className, triggerType, applicationId) { if (!applicationId) { throw "Missing ApplicationID"; From 9bad87905fcdc21e20aace4cea8174bb5b4371a6 Mon Sep 17 00:00:00 2001 From: Tyler Brock Date: Fri, 20 May 2016 14:18:41 -0700 Subject: [PATCH 44/48] Add MongoDB 3.2 to test matrix (#1842) * Add MongoDB 3.2 to test matrix - Updated mongodb-runner to support specifying storage engine - Specifying mmapv1 explictly because of new 3.2 default * Increase jasmine.DEFAULT_TIMEOUT_INTERVAL to 3 seconds * Use fixed mongodb-runner * Increase jasmine.DEFAULT_TIMEOUT_INTERVAL to 15 seconds * Update to mongodb-runner 3.3.2 upon release --- .travis.yml | 1 + package.json | 4 ++-- spec/helper.js | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 68052a1f5b..e6248cfd7e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ env: matrix: - MONGODB_VERSION=2.6.11 - MONGODB_VERSION=3.0.8 + - MONGODB_VERSION=3.2.6 branches: only: - master diff --git a/package.json b/package.json index ac8bc7e037..c8f36227a2 100644 --- a/package.json +++ b/package.json @@ -59,13 +59,13 @@ "deep-diff": "^0.3.3", "gaze": "^1.0.0", "jasmine": "^2.3.2", - "mongodb-runner": "3.2.2", + "mongodb-runner": "^3.3.2", "nodemon": "^1.8.1" }, "scripts": { "dev": "npm run build && node bin/dev", "build": "./node_modules/.bin/babel src/ -d lib/", - "pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.0.8} ./node_modules/.bin/mongodb-runner start", + "pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.2.6} MONGODB_STORAGE_ENGINE=mmapv1 ./node_modules/.bin/mongodb-runner start", "test": "cross-env NODE_ENV=test TESTING=1 ./node_modules/.bin/babel-node $COVERAGE_OPTION ./node_modules/jasmine/bin/jasmine.js", "test:win": "npm run pretest && cross-env NODE_ENV=test TESTING=1 ./node_modules/.bin/babel-node ./node_modules/babel-istanbul/lib/cli.js cover -x **/spec/** ./node_modules/jasmine/bin/jasmine.js && npm run posttest", "posttest": "./node_modules/.bin/mongodb-runner stop", diff --git a/spec/helper.js b/spec/helper.js index 049c9664fa..5d804d12e9 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -1,6 +1,6 @@ // Sets up a Parse API server for testing. -jasmine.DEFAULT_TIMEOUT_INTERVAL = 2000; +jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000; var cache = require('../src/cache').default; var DatabaseAdapter = require('../src/DatabaseAdapter'); From fab8cfdfc736f41396c33e424fe08f67eff346e2 Mon Sep 17 00:00:00 2001 From: Jeremy Pease Date: Fri, 20 May 2016 21:15:47 -0400 Subject: [PATCH 45/48] Add additional default fields to _Installation class (#1852) Fields are appVersion, appName, appIdentifier, and parseVersion. These fields are sent by Android and iOS SDKs. --- spec/MongoSchemaCollectionAdapter.spec.js | 18 ++++++++++++++++++ spec/Schema.spec.js | 4 ++++ src/Controllers/SchemaController.js | 12 ++++++++---- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/spec/MongoSchemaCollectionAdapter.spec.js b/spec/MongoSchemaCollectionAdapter.spec.js index 00a5b1900c..ba22c1fab3 100644 --- a/spec/MongoSchemaCollectionAdapter.spec.js +++ b/spec/MongoSchemaCollectionAdapter.spec.js @@ -28,6 +28,15 @@ describe('MongoSchemaCollection', () => { "deviceType":"string", "channels":"array", "user":"*_User", + "pushType":"string", + "GCMSenderId":"string", + "timeZone":"string", + "localeIdentifier":"string", + "badge":"number", + "appVersion":"string", + "appName":"string", + "appIdentifier":"string", + "parseVersion":"string", })).toEqual({ className: '_Installation', fields: { @@ -36,6 +45,15 @@ describe('MongoSchemaCollection', () => { deviceType: { type: 'String' }, channels: { type: 'Array' }, user: { type: 'Pointer', targetClass: '_User' }, + pushType: { type: 'String' }, + GCMSenderId: { type: 'String' }, + timeZone: { type: 'String' }, + localeIdentifier: { type: 'String' }, + badge: { type: 'Number' }, + appVersion: { type: 'String' }, + appName: { type: 'String' }, + appIdentifier: { type: 'String' }, + parseVersion: { type: 'String' }, ACL: { type: 'ACL' }, createdAt: { type: 'Date' }, updatedAt: { type: 'Date' }, diff --git a/spec/Schema.spec.js b/spec/Schema.spec.js index d824d2b048..e30ed6da49 100644 --- a/spec/Schema.spec.js +++ b/spec/Schema.spec.js @@ -453,6 +453,10 @@ describe('SchemaController', () => { timeZone: { type: 'String' }, localeIdentifier: { type: 'String' }, badge: { type: 'Number' }, + appVersion: { type: 'String' }, + appName: { type: 'String' }, + appIdentifier: { type: 'String' }, + parseVersion: { type: 'String' }, }, classLevelPermissions: { find: { '*': true }, diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index 8c78d1cf57..b3fdc7bb77 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -33,7 +33,7 @@ const defaultColumns = Object.freeze({ "email": {type:'String'}, "emailVerified": {type:'Boolean'}, }, - // The additional default columns for the _User collection (in addition to DefaultCols) + // The additional default columns for the _Installation collection (in addition to DefaultCols) _Installation: { "installationId": {type:'String'}, "deviceToken": {type:'String'}, @@ -43,15 +43,19 @@ const defaultColumns = Object.freeze({ "GCMSenderId": {type:'String'}, "timeZone": {type:'String'}, "localeIdentifier": {type:'String'}, - "badge": {type:'Number'} + "badge": {type:'Number'}, + "appVersion": {type:'String'}, + "appName": {type:'String'}, + "appIdentifier": {type:'String'}, + "parseVersion": {type:'String'}, }, - // The additional default columns for the _User collection (in addition to DefaultCols) + // The additional default columns for the _Role collection (in addition to DefaultCols) _Role: { "name": {type:'String'}, "users": {type:'Relation', targetClass:'_User'}, "roles": {type:'Relation', targetClass:'_Role'} }, - // The additional default columns for the _User collection (in addition to DefaultCols) + // The additional default columns for the _Session collection (in addition to DefaultCols) _Session: { "restricted": {type:'Boolean'}, "user": {type:'Pointer', targetClass:'_User'}, From eefa2ccac7cf5248f5be9396b124b2772cc4a07c Mon Sep 17 00:00:00 2001 From: Drew Date: Sun, 22 May 2016 06:33:59 -0700 Subject: [PATCH 46/48] Handle "bytes" type in DB. Fixes #1821. (#1866) --- src/Adapters/Storage/Mongo/MongoSchemaCollection.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Adapters/Storage/Mongo/MongoSchemaCollection.js b/src/Adapters/Storage/Mongo/MongoSchemaCollection.js index a51edc2a65..11db8a377e 100644 --- a/src/Adapters/Storage/Mongo/MongoSchemaCollection.js +++ b/src/Adapters/Storage/Mongo/MongoSchemaCollection.js @@ -25,6 +25,7 @@ function mongoFieldToParseSchemaField(type) { case 'array': return {type: 'Array'}; case 'geopoint': return {type: 'GeoPoint'}; case 'file': return {type: 'File'}; + case 'bytes': return {type: 'Bytes'}; } } From 392102eb97a8b8a183e635453a14affb7cb32166 Mon Sep 17 00:00:00 2001 From: Drew Date: Sun, 22 May 2016 09:59:36 -0700 Subject: [PATCH 47/48] Cache users by objectID, and clear cache when updated via master key (fixes #1836) (#1844) * Cache users by objectID, and clear cache when updated via master key * Go back to caching by session token. Clear out cache by querying _Session when user is modified with Master Key (ew, hopefully that can be improved later) * Fix issue with user updates from different sessions causing stale reads * Tests aren't transpiled... * Still not transpiled --- spec/CloudCode.spec.js | 90 +++++++++++++++++++++++++++++ src/Adapters/Cache/InMemoryCache.js | 1 - src/Auth.js | 1 - src/RestWrite.js | 22 ++++--- src/Routers/UsersRouter.js | 1 + src/middlewares.js | 20 +++---- 6 files changed, 115 insertions(+), 20 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 2218a25706..8c2802d3e5 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -1,5 +1,7 @@ "use strict" const Parse = require("parse/node"); +const request = require('request'); +const InMemoryCacheAdapter = require('../src/Adapters/Cache/InMemoryCacheAdapter').InMemoryCacheAdapter; describe('Cloud Code', () => { it('can load absolute cloud code file', done => { @@ -467,4 +469,92 @@ describe('Cloud Code', () => { done(); }); }); + + it('doesnt receive stale user in cloud code functions after user has been updated with master key (regression test for #1836)', done => { + Parse.Cloud.define('testQuery', function(request, response) { + response.success(request.user.get('data')); + }); + + Parse.User.signUp('user', 'pass') + .then(user => { + user.set('data', 'AAA'); + return user.save(); + }) + .then(() => Parse.Cloud.run('testQuery')) + .then(result => { + expect(result).toEqual('AAA'); + Parse.User.current().set('data', 'BBB'); + return Parse.User.current().save(null, {useMasterKey: true}); + }) + .then(() => Parse.Cloud.run('testQuery')) + .then(result => { + expect(result).toEqual('BBB'); + done(); + }); + }); + + it('clears out the user cache for all sessions when the user is changed', done => { + const cacheAdapter = new InMemoryCacheAdapter({ ttl: 100000000 }); + setServerConfiguration(Object.assign({}, defaultConfiguration, { cacheAdapter: cacheAdapter })); + Parse.Cloud.define('checkStaleUser', (request, response) => { + response.success(request.user.get('data')); + }); + + let user = new Parse.User(); + user.set('username', 'test'); + user.set('password', 'moon-y'); + user.set('data', 'first data'); + user.signUp() + .then(user => { + let session1 = user.getSessionToken(); + request.get({ + url: 'http://localhost:8378/1/login?username=test&password=moon-y', + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + }, (error, response, body) => { + let session2 = body.sessionToken; + + //Ensure both session tokens are in the cache + Parse.Cloud.run('checkStaleUser') + .then(() => { + request.post({ + url: 'http://localhost:8378/1/functions/checkStaleUser', + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Session-Token': session2, + } + }, (error, response, body) => { + Parse.Promise.all([cacheAdapter.get('test:user:' + session1), cacheAdapter.get('test:user:' + session2)]) + .then(cachedVals => { + expect(cachedVals[0].objectId).toEqual(user.id); + expect(cachedVals[1].objectId).toEqual(user.id); + + //Change with session 1 and then read with session 2. + user.set('data', 'second data'); + user.save() + .then(() => { + request.post({ + url: 'http://localhost:8378/1/functions/checkStaleUser', + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Session-Token': session2, + } + }, (error, response, body) => { + expect(body.result).toEqual('second data'); + done(); + }) + }); + }); + }); + }); + }); + }); + }); }); diff --git a/src/Adapters/Cache/InMemoryCache.js b/src/Adapters/Cache/InMemoryCache.js index 37eeb43b02..2d44292a0a 100644 --- a/src/Adapters/Cache/InMemoryCache.js +++ b/src/Adapters/Cache/InMemoryCache.js @@ -53,7 +53,6 @@ export class InMemoryCache { if (record.timeout) { clearTimeout(record.timeout); } - delete this.cache[key]; } diff --git a/src/Auth.js b/src/Auth.js index 8f21567903..634a839b9a 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -72,7 +72,6 @@ var getAuthForSessionToken = function({ config, sessionToken, installationId } = obj['className'] = '_User'; obj['sessionToken'] = sessionToken; config.cacheController.user.put(sessionToken, obj); - let userObject = Parse.Object.fromJSON(obj); return new Auth({config, isMaster: false, installationId, user: userObject}); }); diff --git a/src/RestWrite.js b/src/RestWrite.js index f6e758f6bf..be460d4c48 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -11,6 +11,7 @@ var cryptoUtils = require('./cryptoUtils'); var passwordCrypto = require('./password'); var Parse = require('parse/node'); var triggers = require('./triggers'); +import RestQuery from './RestQuery'; // query and data are both provided in REST API format. So data // types are encoded by plain old objects. @@ -318,10 +319,17 @@ RestWrite.prototype.transformUser = function() { var promise = Promise.resolve(); - // If we're updating a _User object, clear the user cache for the session - if (this.query && this.auth.user && this.auth.user.getSessionToken()) { - let cacheAdapter = this.config.cacheController; - cacheAdapter.user.del(this.auth.user.getSessionToken()); + if (this.query) { + // If we're updating a _User object, we need to clear out the cache for that user. Find all their + // session tokens, and remove them from the cache. + promise = new RestQuery(this.config, Auth.master(this.config), '_Session', { user: { + __type: "Pointer", + className: "_User", + objectId: this.objectId(), + }}).execute() + .then(results => { + results.results.forEach(session => this.config.cacheController.user.del(session.sessionToken)); + }); } return promise.then(() => { @@ -414,8 +422,7 @@ RestWrite.prototype.createSessionTokenIfNeeded = function() { if (this.response && this.response.response) { this.response.response.sessionToken = token; } - var create = new RestWrite(this.config, Auth.master(this.config), - '_Session', null, sessionData); + var create = new RestWrite(this.config, Auth.master(this.config), '_Session', null, sessionData); return create.execute(); } @@ -482,8 +489,7 @@ RestWrite.prototype.handleSession = function() { } sessionData[key] = this.data[key]; } - var create = new RestWrite(this.config, Auth.master(this.config), - '_Session', null, sessionData); + var create = new RestWrite(this.config, Auth.master(this.config), '_Session', null, sessionData); return create.execute().then((results) => { if (!results.response) { throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index adba752f83..a5e6299c58 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -85,6 +85,7 @@ export class UsersRouter extends ClassesRouter { user = results[0]; return passwordCrypto.compare(req.body.password, user.password); }).then((correct) => { + if (!correct) { throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.'); } diff --git a/src/middlewares.js b/src/middlewares.js index d534215453..e46eb62557 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -27,9 +27,9 @@ function handleParseHeaders(req, res, next) { dotNetKey: req.get('X-Parse-Windows-Key'), restAPIKey: req.get('X-Parse-REST-API-Key') }; - + var basicAuth = httpAuth(req); - + if (basicAuth) { info.appId = basicAuth.appId info.masterKey = basicAuth.masterKey || info.masterKey; @@ -156,24 +156,24 @@ function httpAuth(req) { if (!(req.req || req).headers.authorization) return ; - var header = (req.req || req).headers.authorization; - var appId, masterKey, javascriptKey; + var header = (req.req || req).headers.authorization; + var appId, masterKey, javascriptKey; // parse header var authPrefix = 'basic '; - + var match = header.toLowerCase().indexOf(authPrefix); - + if (match == 0) { var encodedAuth = header.substring(authPrefix.length, header.length); var credentials = decodeBase64(encodedAuth).split(':'); - + if (credentials.length == 2) { appId = credentials[0]; var key = credentials[1]; - + var jsKeyPrefix = 'javascript-key='; - + var matchKey = key.indexOf(jsKeyPrefix) if (matchKey == 0) { javascriptKey = key.substring(jsKeyPrefix.length, key.length); @@ -183,7 +183,7 @@ function httpAuth(req) { } } } - + return {appId: appId, masterKey: masterKey, javascriptKey: javascriptKey}; } From e93a1ad7fc7bd49c8289e92bc55c1c6ff65e457c Mon Sep 17 00:00:00 2001 From: Drew Date: Mon, 23 May 2016 09:15:32 -0700 Subject: [PATCH 48/48] Pin mongodb package Fixes #1855 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c8f36227a2..c767ca7177 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "lru-cache": "^4.0.0", "mailgun-js": "^0.7.7", "mime": "^1.3.4", - "mongodb": "~2.1.0", + "mongodb": "2.1.18", "multer": "^1.1.0", "parse": "^1.8.0", "parse-server-fs-adapter": "^1.0.0",