From 295d361725dd69e32dc25937404394fc757d918d Mon Sep 17 00:00:00 2001 From: Nikita Lutsenko Date: Fri, 4 Mar 2016 01:02:44 -0800 Subject: [PATCH 01/18] Convert Schema.js to ES6 class. --- spec/Schema.spec.js | 24 +- src/Schema.js | 781 ++++++++++++++++++++++---------------------- 2 files changed, 402 insertions(+), 403 deletions(-) diff --git a/spec/Schema.spec.js b/spec/Schema.spec.js index b5613c1d..62ce711e 100644 --- a/spec/Schema.spec.js +++ b/spec/Schema.spec.js @@ -180,20 +180,18 @@ describe('Schema', () => { it('will fail to create a class if that class was already created by an object', done => { config.database.loadSchema() - .then(schema => { - schema.validateObject('NewClass', {foo: 7}) - .then(() => { - schema.reload() - .then(schema => schema.addClassIfNotExists('NewClass', { - foo: {type: 'String'} - })) - .catch(error => { - expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME); - expect(error.message).toEqual('Class NewClass already exists.'); - done(); - }); + .then(schema => { + schema.validateObject('NewClass', { foo: 7 }) + .then(() => schema.reloadData()) + .then(() => schema.addClassIfNotExists('NewClass', { + foo: { type: 'String' } + })) + .catch(error => { + expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME); + expect(error.message).toEqual('Class NewClass already exists.'); + done(); + }); }); - }) }); it('will resolve class creation races appropriately', done => { diff --git a/src/Schema.js b/src/Schema.js index 755249bf..73eaa325 100644 --- a/src/Schema.js +++ b/src/Schema.js @@ -71,7 +71,6 @@ var defaultColumns = { } }; - var requiredColumns = { _Product: ["productIdentifier", "icon", "order", "title", "subtitle"], _Role: ["name", "ACL"] @@ -168,54 +167,376 @@ function schemaAPITypeToMongoFieldType(type) { // '_id' indicates the className // '_metadata' is ignored for now // Everything else is expected to be a userspace field. -function Schema(collection, mongoSchema) { - this.collection = collection; +class Schema { + collection; + data; + perms; - // this.data[className][fieldName] tells you the type of that field - this.data = {}; - // this.perms[className][operation] tells you the acl-style permissions - this.perms = {}; + constructor(collection) { + this.collection = collection; - for (var obj of mongoSchema) { - var className = null; - var classData = {}; - var permsData = null; - for (var key in obj) { - var value = obj[key]; - switch(key) { - case '_id': - className = value; - break; - case '_metadata': - if (value && value['class_permissions']) { - permsData = value['class_permissions']; - } - break; - default: - classData[key] = value; - } - } - if (className) { - this.data[className] = classData; - if (permsData) { - this.perms[className] = permsData; - } - } + // this.data[className][fieldName] tells you the type of that field + this.data = {}; + // this.perms[className][operation] tells you the acl-style permissions + this.perms = {}; } + + reloadData() { + this.data = {}; + this.perms = {}; + return this.collection.find({}, {}).toArray().then(mongoSchema => { + for (let obj of mongoSchema) { + let className = null; + let classData = {}; + let permsData = null; + Object.keys(obj).forEach(key => { + let value = obj[key]; + switch (key) { + case '_id': + className = value; + break; + case '_metadata': + if (value && value['class_permissions']) { + permsData = value['class_permissions']; + } + break; + default: + classData[key] = value; + } + }); + if (className) { + this.data[className] = classData; + if (permsData) { + this.perms[className] = permsData; + } + } + } + }); + } + + // Create a new class that includes the three default fields. + // ACL is an implicit column that does not get an entry in the + // _SCHEMAS database. Returns a promise that resolves with the + // created schema, in mongo format. + // on success, and rejects with an error on fail. Ensure you + // have authorization (master key, or client class creation + // enabled) before calling this function. + addClassIfNotExists(className, fields) { + if (this.data[className]) { + throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`); + } + + let mongoObject = mongoSchemaFromFieldsAndClassName(fields, className); + if (!mongoObject.result) { + return Promise.reject(mongoObject); + } + + return this.collection.insertOne(mongoObject.result) + .then(result => result.ops[0]) + .catch(error => { + if (error.code === 11000) { //Mongo's duplicate key error + throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`); + } + return Promise.reject(error); + }); + } + + + // Returns whether the schema knows the type of all these keys. + hasKeys(className, keys) { + for (var key of keys) { + if (!this.data[className] || !this.data[className][key]) { + return false; + } + } + return true; + } + + // Returns a promise that resolves successfully to the new schema + // object or fails with a reason. + // If 'freeze' is true, refuse to update the schema. + // WARNING: this function has side-effects, and doesn't actually + // do any validation of the format of the className. You probably + // should use classNameIsValid or addClassIfNotExists or something + // like that instead. TODO: rename or remove this function. + validateClassName(className, freeze) { + if (this.data[className]) { + return Promise.resolve(this); + } + if (freeze) { + throw new Parse.Error(Parse.Error.INVALID_JSON, + 'schema is frozen, cannot add: ' + className); + } + // We don't have this class. Update the schema + return this.collection.insert([{_id: className}]).then(() => { + // The schema update succeeded. Reload the schema + return this.reloadData(); + }, () => { + // The schema update failed. This can be okay - it might + // have failed because there's a race condition and a different + // client is making the exact same schema update that we want. + // So just reload the schema. + return this.reloadData(); + }).then(() => { + // Ensure that the schema now validates + return this.validateClassName(className, true); + }, (error) => { + // The schema still doesn't validate. Give up + throw new Parse.Error(Parse.Error.INVALID_JSON, + 'schema class name does not revalidate'); + }); + } + + // Sets the Class-level permissions for a given className, which must exist. + setPermissions(className, perms) { + var query = {_id: className}; + var update = { + _metadata: { + class_permissions: perms + } + }; + update = {'$set': update}; + return this.collection.findAndModify(query, {}, update, {}).then(() => { + // The update succeeded. Reload the schema + return this.reloadData(); + }); + } + + // Returns a promise that resolves successfully to the new schema + // object if the provided className-key-type tuple is valid. + // The className must already be validated. + // If 'freeze' is true, refuse to update the schema for this field. + validateField(className, key, type, freeze) { + // Just to check that the key is valid + transform.transformKey(this, className, key); + + if( key.indexOf(".") > 0 ) { + // subdocument key (x.y) => ok if x is of type 'object' + key = key.split(".")[ 0 ]; + type = 'object'; + } + + var expected = this.data[className][key]; + if (expected) { + expected = (expected === 'map' ? 'object' : expected); + if (expected === type) { + return Promise.resolve(this); + } else { + throw new Parse.Error( + Parse.Error.INCORRECT_TYPE, + 'schema mismatch for ' + className + '.' + key + + '; expected ' + expected + ' but got ' + type); + } + } + + if (freeze) { + throw new Parse.Error(Parse.Error.INVALID_JSON, + 'schema is frozen, cannot add ' + key + ' field'); + } + + // We don't have this field, but if the value is null or undefined, + // we won't update the schema until we get a value with a type. + if (!type) { + return Promise.resolve(this); + } + + if (type === 'geopoint') { + // Make sure there are not other geopoint fields + for (var otherKey in this.data[className]) { + if (this.data[className][otherKey] === 'geopoint') { + throw new Parse.Error( + Parse.Error.INCORRECT_TYPE, + 'there can only be one geopoint field in a class'); + } + } + } + + // We don't have this field. Update the schema. + // Note that we use the $exists guard and $set to avoid race + // conditions in the database. This is important! + var query = {_id: className}; + query[key] = {'$exists': false}; + var update = {}; + update[key] = type; + update = {'$set': update}; + return this.collection.findAndModify(query, {}, update, {}).then(() => { + // The update succeeded. Reload the schema + return this.reloadData(); + }, () => { + // The update failed. This can be okay - it might have been a race + // condition where another client updated the schema in the same + // way that we wanted to. So, just reload the schema + return this.reloadData(); + }).then(() => { + // Ensure that the schema now validates + return this.validateField(className, key, type, true); + }, (error) => { + // The schema still doesn't validate. Give up + throw new Parse.Error(Parse.Error.INVALID_JSON, + 'schema key will not revalidate'); + }); + } + + // Delete a field, and remove that data from all objects. This is intended + // to remove unused fields, if other writers are writing objects that include + // this field, the field may reappear. Returns a Promise that resolves with + // no object on success, or rejects with { code, error } on failure. + // Passing the database and prefix is necessary in order to drop relation collections + // and remove fields from objects. Ideally the database would belong to + // a database adapter and this function would close over it or access it via member. + deleteField(fieldName, className, database) { + if (!classNameIsValid(className)) { + throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, invalidClassNameMessage(className)); + } + if (!fieldNameIsValid(fieldName)) { + throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `invalid field name: ${fieldName}`); + } + //Don't allow deleting the default fields. + if (!fieldNameIsValidForClass(fieldName, className)) { + throw new Parse.Error(136, `field ${fieldName} cannot be changed`); + } + + return this.reloadData() + .then(() => { + return this.hasClass(className) + .then(hasClass => { + if (!hasClass) { + throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} does not exist.`); + } + if (!this.data[className][fieldName]) { + throw new Parse.Error(255, `Field ${fieldName} does not exist, cannot delete.`); + } + + if (this.data[className][fieldName].startsWith('relation<')) { + //For relations, drop the _Join table + return database.dropCollection(`_Join:${fieldName}:${className}`); + } + + // for non-relations, remove all the data. + // This is necessary to ensure that the data is still gone if they add the same field. + return database.collection(className) + .then(collection => { + var mongoFieldName = this.data[className][fieldName].startsWith('*') ? '_p_' + fieldName : fieldName; + return collection.update({}, { "$unset": { [mongoFieldName] : null } }, { multi: true }); + }); + }) + // Save the _SCHEMA object + .then(() => this.collection.update({ _id: className }, { $unset: {[fieldName]: null }})); + }); + } + + // Validates an object provided in REST format. + // Returns a promise that resolves to the new schema if this object is + // valid. + validateObject(className, object, query) { + var geocount = 0; + var promise = this.validateClassName(className); + for (var key in object) { + if (object[key] === undefined) { + continue; + } + var expected = getType(object[key]); + if (expected === 'geopoint') { + geocount++; + } + if (geocount > 1) { + throw new Parse.Error( + Parse.Error.INCORRECT_TYPE, + 'there can only be one geopoint field in a class'); + } + if (!expected) { + continue; + } + promise = thenValidateField(promise, className, key, expected); + } + promise = thenValidateRequiredColumns(promise, className, object, query); + return promise; + } + + // Validates that all the properties are set for the object + validateRequiredColumns(className, object, query) { + var columns = requiredColumns[className]; + if (!columns || columns.length == 0) { + return Promise.resolve(this); + } + + var missingColumns = columns.filter(function(column){ + if (query && query.objectId) { + if (object[column] && typeof object[column] === "object") { + // Trying to delete a required column + return object[column].__op == 'Delete'; + } + // Not trying to do anything there + return false; + } + return !object[column] + }); + + if (missingColumns.length > 0) { + throw new Parse.Error( + Parse.Error.INCORRECT_TYPE, + missingColumns[0]+' is required.'); + } + + return Promise.resolve(this); + } + + // Validates an operation passes class-level-permissions set in the schema + validatePermission(className, aclGroup, operation) { + if (!this.perms[className] || !this.perms[className][operation]) { + return Promise.resolve(); + } + var perms = this.perms[className][operation]; + // Handle the public scenario quickly + if (perms['*']) { + return Promise.resolve(); + } + // Check permissions against the aclGroup provided (array of userId/roles) + var found = false; + for (var i = 0; i < aclGroup.length && !found; i++) { + if (perms[aclGroup[i]]) { + found = true; + } + } + if (!found) { + // TODO: Verify correct error code + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, + 'Permission denied for this action.'); + } + }; + + // Returns the expected type for a className+key combination + // or undefined if the schema is not set + getExpectedType(className, key) { + if (this.data && this.data[className]) { + return this.data[className][key]; + } + return undefined; + }; + + // Checks if a given class is in the schema. Needs to load the + // schema first, which is kinda janky. Hopefully we can refactor + // and make this be a regular value. + hasClass(className) { + return this.reloadData().then(() => !!(this.data[className])); + } + + // Helper function to check if a field is a pointer, returns true or false. + isPointer(className, key) { + var expected = this.getExpectedType(className, key); + if (expected && expected.charAt(0) == '*') { + return true; + } + return false; + }; } // Returns a promise for a new Schema. function load(collection) { - return collection.find({}, {}).toArray().then((mongoSchema) => { - return new Schema(collection, mongoSchema); - }); + let schema = new Schema(collection); + return schema.reloadData().then(() => schema); } -// Returns a new, reloaded schema. -Schema.prototype.reload = function() { - return load(this.collection); -}; - // Returns { code, error } if invalid, or { result }, an object // suitable for inserting into _SCHEMA collection, otherwise function mongoSchemaFromFieldsAndClassName(fields, className) { @@ -331,218 +652,6 @@ function buildMergedSchemaObject(mongoObject, putRequest) { return newSchema; } -// Create a new class that includes the three default fields. -// ACL is an implicit column that does not get an entry in the -// _SCHEMAS database. Returns a promise that resolves with the -// created schema, in mongo format. -// on success, and rejects with an error on fail. Ensure you -// have authorization (master key, or client class creation -// enabled) before calling this function. -Schema.prototype.addClassIfNotExists = function(className, fields) { - if (this.data[className]) { - throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`); - } - - let mongoObject = mongoSchemaFromFieldsAndClassName(fields, className); - if (!mongoObject.result) { - return Promise.reject(mongoObject); - } - - return this.collection.insertOne(mongoObject.result) - .then(result => result.ops[0]) - .catch(error => { - if (error.code === 11000) { //Mongo's duplicate key error - throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`); - } - return Promise.reject(error); - }); -}; - -// Returns a promise that resolves successfully to the new schema -// object or fails with a reason. -// If 'freeze' is true, refuse to update the schema. -// WARNING: this function has side-effects, and doesn't actually -// do any validation of the format of the className. You probably -// should use classNameIsValid or addClassIfNotExists or something -// like that instead. TODO: rename or remove this function. -Schema.prototype.validateClassName = function(className, freeze) { - if (this.data[className]) { - return Promise.resolve(this); - } - if (freeze) { - throw new Parse.Error(Parse.Error.INVALID_JSON, - 'schema is frozen, cannot add: ' + className); - } - // We don't have this class. Update the schema - return this.collection.insert([{_id: className}]).then(() => { - // The schema update succeeded. Reload the schema - return this.reload(); - }, () => { - // The schema update failed. This can be okay - it might - // have failed because there's a race condition and a different - // client is making the exact same schema update that we want. - // So just reload the schema. - return this.reload(); - }).then((schema) => { - // Ensure that the schema now validates - return schema.validateClassName(className, true); - }, (error) => { - // The schema still doesn't validate. Give up - throw new Parse.Error(Parse.Error.INVALID_JSON, - 'schema class name does not revalidate'); - }); -}; - -// Returns whether the schema knows the type of all these keys. -Schema.prototype.hasKeys = function(className, keys) { - for (var key of keys) { - if (!this.data[className] || !this.data[className][key]) { - return false; - } - } - return true; -}; - -// Sets the Class-level permissions for a given className, which must -// exist. -Schema.prototype.setPermissions = function(className, perms) { - var query = {_id: className}; - var update = { - _metadata: { - class_permissions: perms - } - }; - update = {'$set': update}; - return this.collection.findAndModify(query, {}, update, {}).then(() => { - // The update succeeded. Reload the schema - return this.reload(); - }); -}; - -// Returns a promise that resolves successfully to the new schema -// object if the provided className-key-type tuple is valid. -// The className must already be validated. -// If 'freeze' is true, refuse to update the schema for this field. -Schema.prototype.validateField = function(className, key, type, freeze) { - // Just to check that the key is valid - transform.transformKey(this, className, key); - - if( key.indexOf(".") > 0 ) { - // subdocument key (x.y) => ok if x is of type 'object' - key = key.split(".")[ 0 ]; - type = 'object'; - } - - var expected = this.data[className][key]; - if (expected) { - expected = (expected === 'map' ? 'object' : expected); - if (expected === type) { - return Promise.resolve(this); - } else { - throw new Parse.Error( - Parse.Error.INCORRECT_TYPE, - 'schema mismatch for ' + className + '.' + key + - '; expected ' + expected + ' but got ' + type); - } - } - - if (freeze) { - throw new Parse.Error(Parse.Error.INVALID_JSON, - 'schema is frozen, cannot add ' + key + ' field'); - } - - // We don't have this field, but if the value is null or undefined, - // we won't update the schema until we get a value with a type. - if (!type) { - return Promise.resolve(this); - } - - if (type === 'geopoint') { - // Make sure there are not other geopoint fields - for (var otherKey in this.data[className]) { - if (this.data[className][otherKey] === 'geopoint') { - throw new Parse.Error( - Parse.Error.INCORRECT_TYPE, - 'there can only be one geopoint field in a class'); - } - } - } - - // We don't have this field. Update the schema. - // Note that we use the $exists guard and $set to avoid race - // conditions in the database. This is important! - var query = {_id: className}; - query[key] = {'$exists': false}; - var update = {}; - update[key] = type; - update = {'$set': update}; - return this.collection.findAndModify(query, {}, update, {}).then(() => { - // The update succeeded. Reload the schema - return this.reload(); - }, () => { - // The update failed. This can be okay - it might have been a race - // condition where another client updated the schema in the same - // way that we wanted to. So, just reload the schema - return this.reload(); - }).then((schema) => { - // Ensure that the schema now validates - return schema.validateField(className, key, type, true); - }, (error) => { - // The schema still doesn't validate. Give up - throw new Parse.Error(Parse.Error.INVALID_JSON, - 'schema key will not revalidate'); - }); -}; - -// Delete a field, and remove that data from all objects. This is intended -// to remove unused fields, if other writers are writing objects that include -// this field, the field may reappear. Returns a Promise that resolves with -// no object on success, or rejects with { code, error } on failure. - -// Passing the database and prefix is necessary in order to drop relation collections -// and remove fields from objects. Ideally the database would belong to -// a database adapter and this function would close over it or access it via member. -Schema.prototype.deleteField = function(fieldName, className, database) { - if (!classNameIsValid(className)) { - throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, invalidClassNameMessage(className)); - } - if (!fieldNameIsValid(fieldName)) { - throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `invalid field name: ${fieldName}`); - } - //Don't allow deleting the default fields. - if (!fieldNameIsValidForClass(fieldName, className)) { - throw new Parse.Error(136, `field ${fieldName} cannot be changed`); - } - - return this.reload() - .then(schema => { - return schema.hasClass(className) - .then(hasClass => { - if (!hasClass) { - throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} does not exist.`); - } - if (!schema.data[className][fieldName]) { - throw new Parse.Error(255, `Field ${fieldName} does not exist, cannot delete.`); - } - - if (schema.data[className][fieldName].startsWith('relation<')) { - //For relations, drop the _Join table - return database.dropCollection(`_Join:${fieldName}:${className}`); - } - - // for non-relations, remove all the data. - // This is necessary to ensure that the data is still gone if they add the same field. - return database.collection(className) - .then(collection => { - var mongoFieldName = schema.data[className][fieldName].startsWith('*') ? '_p_' + fieldName : fieldName; - return collection.update({}, { "$unset": { [mongoFieldName] : null } }, { multi: true }); - }); - }) - // Save the _SCHEMA object - .then(() => this.collection.update({ _id: className }, { $unset: {[fieldName]: null }})); - }); -}; - // Given a schema promise, construct another schema promise that // validates this field once the schema loads. function thenValidateField(schemaPromise, className, key, type) { @@ -551,34 +660,6 @@ function thenValidateField(schemaPromise, className, key, type) { }); } -// Validates an object provided in REST format. -// Returns a promise that resolves to the new schema if this object is -// valid. -Schema.prototype.validateObject = function(className, object, query) { - var geocount = 0; - var promise = this.validateClassName(className); - for (var key in object) { - if (object[key] === undefined) { - continue; - } - var expected = getType(object[key]); - if (expected === 'geopoint') { - geocount++; - } - if (geocount > 1) { - throw new Parse.Error( - Parse.Error.INCORRECT_TYPE, - 'there can only be one geopoint field in a class'); - } - if (!expected) { - continue; - } - promise = thenValidateField(promise, className, key, expected); - } - promise = thenValidateRequiredColumns(promise, className, object, query); - return promise; -}; - // Given a schema promise, construct another schema promise that // validates this field once the schema loads. function thenValidateRequiredColumns(schemaPromise, className, object, query) { @@ -587,85 +668,6 @@ function thenValidateRequiredColumns(schemaPromise, className, object, query) { }); } -// Validates that all the properties are set for the object -Schema.prototype.validateRequiredColumns = function(className, object, query) { - - var columns = requiredColumns[className]; - if (!columns || columns.length == 0) { - return Promise.resolve(this); - } - - var missingColumns = columns.filter(function(column){ - if (query && query.objectId) { - if (object[column] && typeof object[column] === "object") { - // Trying to delete a required column - return object[column].__op == 'Delete'; - } - // Not trying to do anything there - return false; - } - return !object[column] - }); - - if (missingColumns.length > 0) { - throw new Parse.Error( - Parse.Error.INCORRECT_TYPE, - missingColumns[0]+' is required.'); - } - - return Promise.resolve(this); -} - - -// Validates an operation passes class-level-permissions set in the schema -Schema.prototype.validatePermission = function(className, aclGroup, operation) { - if (!this.perms[className] || !this.perms[className][operation]) { - return Promise.resolve(); - } - var perms = this.perms[className][operation]; - // Handle the public scenario quickly - if (perms['*']) { - return Promise.resolve(); - } - // Check permissions against the aclGroup provided (array of userId/roles) - var found = false; - for (var i = 0; i < aclGroup.length && !found; i++) { - if (perms[aclGroup[i]]) { - found = true; - } - } - if (!found) { - // TODO: Verify correct error code - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, - 'Permission denied for this action.'); - } -}; - -// Returns the expected type for a className+key combination -// or undefined if the schema is not set -Schema.prototype.getExpectedType = function(className, key) { - if (this.data && this.data[className]) { - return this.data[className][key]; - } - return undefined; -}; - -// Checks if a given class is in the schema. Needs to load the -// schema first, which is kinda janky. Hopefully we can refactor -// and make this be a regular value. -Schema.prototype.hasClass = function(className) { - return this.reload().then(newSchema => !!newSchema.data[className]); -} - -// Helper function to check if a field is a pointer, returns true or false. -Schema.prototype.isPointer = function(className, key) { - var expected = this.getExpectedType(className, key); - if (expected && expected.charAt(0) == '*') { - return true; - } - return false; -}; - // Gets the type from a REST API formatted object, where 'type' is // extended past javascript types to include the rest of the Parse // type system. @@ -674,21 +676,21 @@ Schema.prototype.isPointer = function(className, key) { function getType(obj) { var type = typeof obj; switch(type) { - case 'boolean': - case 'string': - case 'number': - return type; - case 'map': - case 'object': - if (!obj) { - return undefined; - } - return getObjectType(obj); - case 'function': - case 'symbol': - case 'undefined': - default: - throw 'bad obj: ' + obj; + case 'boolean': + case 'string': + case 'number': + return type; + case 'map': + case 'object': + if (!obj) { + return undefined; + } + return getObjectType(obj); + case 'function': + case 'symbol': + case 'undefined': + default: + throw 'bad obj: ' + obj; } } @@ -730,27 +732,26 @@ function getObjectType(obj) { } if (obj.__op) { switch(obj.__op) { - case 'Increment': - return 'number'; - case 'Delete': - return null; - case 'Add': - case 'AddUnique': - case 'Remove': - return 'array'; - case 'AddRelation': - case 'RemoveRelation': - return 'relation<' + obj.objects[0].className + '>'; - case 'Batch': - return getObjectType(obj.ops[0]); - default: - throw 'unexpected op: ' + obj.__op; + case 'Increment': + return 'number'; + case 'Delete': + return null; + case 'Add': + case 'AddUnique': + case 'Remove': + return 'array'; + case 'AddRelation': + case 'RemoveRelation': + return 'relation<' + obj.objects[0].className + '>'; + case 'Batch': + return getObjectType(obj.ops[0]); + default: + throw 'unexpected op: ' + obj.__op; } } return 'object'; } - module.exports = { load: load, classNameIsValid: classNameIsValid, From 8814e1f3b59e0b5bca41b0b014b47838860d6bba Mon Sep 17 00:00:00 2001 From: Marco129 Date: Fri, 4 Mar 2016 20:44:21 +0800 Subject: [PATCH 02/18] Fix add field to system schema --- spec/schemas.spec.js | 57 ++++++++++++++++++++++++++++++++++++ src/Routers/SchemasRouter.js | 2 +- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index 63843e57..9a410eed 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -561,6 +561,63 @@ describe('schemas', () => { }) }); + it('lets you add fields to system schema', done => { + request.post({ + url: 'http://localhost:8378/1/schemas/_User', + headers: masterKeyHeaders, + json: true + }, (error, response, body) => { + request.put({ + url: 'http://localhost:8378/1/schemas/_User', + headers: masterKeyHeaders, + json: true, + body: { + fields: { + newField: {type: 'String'} + } + } + }, (error, response, body) => { + expect(body).toEqual({ + className: '_User', + fields: { + objectId: {type: 'String'}, + updatedAt: {type: 'Date'}, + createdAt: {type: 'Date'}, + username: {type: 'String'}, + password: {type: 'String'}, + authData: {type: 'Object'}, + email: {type: 'String'}, + emailVerified: {type: 'Boolean'}, + newField: {type: 'String'}, + ACL: {type: 'ACL'} + } + }); + request.get({ + url: 'http://localhost:8378/1/schemas/_User', + headers: masterKeyHeaders, + json: true + }, (error, response, body) => { + expect(body).toEqual({ + className: '_User', + fields: { + objectId: {type: 'String'}, + updatedAt: {type: 'Date'}, + createdAt: {type: 'Date'}, + username: {type: 'String'}, + password: {type: 'String'}, + authData: {type: 'Object'}, + email: {type: 'String'}, + emailVerified: {type: 'Boolean'}, + newField: {type: 'String'}, + ACL: {type: 'ACL'} + } + }); + done(); + }); + }); + }) + }); + it('lets you delete multiple fields and add fields', done => { var obj1 = hasAllPODobject(); obj1.save() diff --git a/src/Routers/SchemasRouter.js b/src/Routers/SchemasRouter.js index e352bd5e..59fef02d 100644 --- a/src/Routers/SchemasRouter.js +++ b/src/Routers/SchemasRouter.js @@ -85,7 +85,7 @@ function modifySchema(req) { throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${req.params.className} does not exist.`); } - let existingFields = schema.data[className]; + let existingFields = Object.assign(schema.data[className], {_id: className}); Object.keys(submittedFields).forEach(name => { let field = submittedFields[name]; if (existingFields[name] && field.__op !== 'Delete') { From 1a17d16dc96a7df70bfc3513ab017606949ced5f Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Fri, 4 Mar 2016 09:37:44 -0500 Subject: [PATCH 03/18] Better support for windows builds --- .travis.yml | 2 +- package.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 53dc9acc..2e85f31e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ node_js: - "4.3" env: global: - - CODE_COVERAGE=1 + - COVERAGE_OPTION='./node_modules/babel-istanbul/lib/cli.js cover -x **/spec/**' matrix: - MONGODB_VERSION=2.6.11 - MONGODB_VERSION=3.0.8 diff --git a/package.json b/package.json index f2f863f0..4331ea25 100644 --- a/package.json +++ b/package.json @@ -56,8 +56,9 @@ "dev": "npm run build && 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", - "test": "cross-env NODE_ENV=test TESTING=1 ./node_modules/.bin/babel-node $(if [ \"$CODE_COVERAGE\" = \"1\" ]; then echo './node_modules/babel-istanbul/lib/cli.js cover -x **/spec/**'; fi;) ./node_modules/jasmine/bin/jasmine.js", + "test": "cross-env NODE_ENV=test TESTING=1 ./node_modules/.bin/babel-node $COVERAGE_OPTION ./node_modules/jasmine/bin/jasmine.js", "posttest": "mongodb-runner stop", + "coverage": "cross-env COVERAGE_OPTION='./node_modules/babel-istanbul/lib/cli.js cover -x **/spec/**' npm test", "start": "./bin/parse-server", "prepublish": "npm run build" }, From ee38ebc4a3f41d52e08d521a2c7707b443805570 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Fri, 4 Mar 2016 09:40:47 -0500 Subject: [PATCH 04/18] Removes shebang for windows --- bin/parse-server | 1 - package.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/bin/parse-server b/bin/parse-server index d3ade1e8..94ffe964 100755 --- a/bin/parse-server +++ b/bin/parse-server @@ -1,2 +1 @@ -#!/usr/bin/env node require("../lib/cli/parse-server"); diff --git a/package.json b/package.json index 4331ea25..d2a0294c 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "test": "cross-env NODE_ENV=test TESTING=1 ./node_modules/.bin/babel-node $COVERAGE_OPTION ./node_modules/jasmine/bin/jasmine.js", "posttest": "mongodb-runner stop", "coverage": "cross-env COVERAGE_OPTION='./node_modules/babel-istanbul/lib/cli.js cover -x **/spec/**' npm test", - "start": "./bin/parse-server", + "start": "node ./bin/parse-server", "prepublish": "npm run build" }, "engines": { From 2c844a11b9e4adac8f9e02f08b0787bfc08e8bb8 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Fri, 4 Mar 2016 17:01:14 -0500 Subject: [PATCH 05/18] Adds public_html and views for packaging --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index d2a0294c..29e873da 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "files": [ "bin/", "lib/", + "public_html/", + "views/", "LICENSE", "PATENTS", "README.md" From 069605e9c3ef9c457d044e53cf53273cd0300606 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Fri, 4 Mar 2016 10:54:32 -0500 Subject: [PATCH 06/18] Improves loading of Push Adapter, fix loading of S3Adapter - Adds environment variables to configure S3Adapter --- src/Adapters/AdapterLoader.js | 7 ------- src/Adapters/Files/S3Adapter.js | 35 +++++++++++++++++---------------- src/index.js | 3 ++- 3 files changed, 20 insertions(+), 25 deletions(-) diff --git a/src/Adapters/AdapterLoader.js b/src/Adapters/AdapterLoader.js index 5b46f22d..fd6741c2 100644 --- a/src/Adapters/AdapterLoader.js +++ b/src/Adapters/AdapterLoader.js @@ -28,13 +28,6 @@ export function loadAdapter(adapter, defaultAdapter, options) { return loadAdapter(adapter.class, undefined, adapter.options); } else if (adapter.adapter) { return loadAdapter(adapter.adapter, undefined, adapter.options); - } else { - // Try to load the defaultAdapter with the options - // The default adapter should throw if the options are - // incompatible - try { - return loadAdapter(defaultAdapter, undefined, adapter); - } catch (e) {}; } // return the adapter as is as it's unusable otherwise return adapter; diff --git a/src/Adapters/Files/S3Adapter.js b/src/Adapters/Files/S3Adapter.js index e21ef8db..cbdf3f11 100644 --- a/src/Adapters/Files/S3Adapter.js +++ b/src/Adapters/Files/S3Adapter.js @@ -8,19 +8,20 @@ import requiredParameter from '../../requiredParameter'; const DEFAULT_S3_REGION = "us-east-1"; -function parseS3AdapterOptions(...options) { - if (options.length === 1 && typeof options[0] == "object") { - return options; +function requiredOrFromEnvironment(env, name) { + let environmentVariable = process.env[env]; + if (!environmentVariable) { + requiredParameter(`S3Adapter requires an ${name}`); } - - const additionalOptions = options[3] || {}; - - return { - accessKey: options[0], - secretKey: options[1], - bucket: options[2], - region: additionalOptions.region + return environmentVariable; +} + +function fromEnvironmentOrDefault(env, defaultValue) { + let environmentVariable = process.env[env]; + if (environmentVariable) { + return environmentVariable; } + return defaultValue; } export class S3Adapter extends FilesAdapter { @@ -28,12 +29,12 @@ export class S3Adapter extends FilesAdapter { // Providing AWS access and secret keys is mandatory // Region and bucket will use sane defaults if omitted constructor( - accessKey = requiredParameter('S3Adapter requires an accessKey'), - secretKey = requiredParameter('S3Adapter requires a secretKey'), - bucket, - { region = DEFAULT_S3_REGION, - bucketPrefix = '', - directAccess = false } = {}) { + accessKey = requiredOrFromEnvironment('S3_ACCESS_KEY', 'accessKey'), + secretKey = requiredOrFromEnvironment('S3_SECRET_KEY', 'secretKey'), + bucket = fromEnvironmentOrDefault('S3_BUCKET', undefined), + { region = fromEnvironmentOrDefault('S3_REGION', DEFAULT_S3_REGION), + bucketPrefix = fromEnvironmentOrDefault('S3_BUCKET_PREFIX', ''), + directAccess = fromEnvironmentOrDefault('S3_DIRECT_ACCESS', false) } = {}) { super(); this._region = region; diff --git a/src/index.js b/src/index.js index 076035f8..4f4b763a 100644 --- a/src/index.js +++ b/src/index.js @@ -133,7 +133,8 @@ function ParseServer({ const filesControllerAdapter = loadAdapter(filesAdapter, () => { return new GridStoreAdapter(databaseURI); }); - const pushControllerAdapter = loadAdapter(push, ParsePushAdapter); + // Pass the push options too as it works with the default + const pushControllerAdapter = loadAdapter(push && push.adapter, ParsePushAdapter, push); const loggerControllerAdapter = loadAdapter(loggerAdapter, FileLoggerAdapter); const emailControllerAdapter = loadAdapter(emailAdapter); // We pass the options and the base class for the adatper, From a44b1d9f76512e38adebbea95de15e7a4170e12d Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Fri, 4 Mar 2016 12:04:41 -0500 Subject: [PATCH 07/18] Improves documentation, add loading tests --- README.md | 14 ++++++++++++++ spec/AdapterLoader.spec.js | 25 +++++++++++++++++++++++++ src/Adapters/AdapterLoader.js | 2 +- 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 84538c4f..b0a8d015 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,20 @@ PARSE_SERVER_MAX_UPLOAD_SIZE ``` +##### Configuring S3 Adapter + +You can use the following environment variable setup the S3 adapter + +```js +S3_ACCESS_KEY +S3_SECRET_KEY +S3_BUCKET +S3_REGION +S3_BUCKET_PREFIX +S3_DIRECT_ACCESS + +``` + ## Contributing We really want Parse to be yours, to see it grow and thrive in the open source community. Please see the [Contributing to Parse Server guide](CONTRIBUTING.md). diff --git a/spec/AdapterLoader.spec.js b/spec/AdapterLoader.spec.js index f32867e0..69381fc5 100644 --- a/spec/AdapterLoader.spec.js +++ b/spec/AdapterLoader.spec.js @@ -1,6 +1,8 @@ var loadAdapter = require("../src/Adapters/AdapterLoader").loadAdapter; var FilesAdapter = require("../src/Adapters/Files/FilesAdapter").default; +var ParsePushAdapter = require("../src/Adapters/Push/ParsePushAdapter"); +var S3Adapter = require("../src/Adapters/Files/S3Adapter").default; describe("AdapterLoader", ()=>{ @@ -84,4 +86,27 @@ describe("AdapterLoader", ()=>{ }).not.toThrow("foo is required for that adapter"); done(); }); + + it("should load push adapter from options", (done) => { + var options = { + ios: { + bundleId: 'bundle.id' + } + } + expect(() => { + var adapter = loadAdapter(undefined, ParsePushAdapter, options); + expect(adapter.constructor).toBe(ParsePushAdapter); + expect(adapter).not.toBe(undefined); + }).not.toThrow(); + done(); + }); + + it("should load S3Adapter from direct passing", (done) => { + var s3Adapter = new S3Adapter("key", "secret", "bucket") + expect(() => { + var adapter = loadAdapter(s3Adapter, FilesAdapter); + expect(adapter).toBe(s3Adapter); + }).not.toThrow(); + done(); + }) }); diff --git a/src/Adapters/AdapterLoader.js b/src/Adapters/AdapterLoader.js index fd6741c2..a9521f0b 100644 --- a/src/Adapters/AdapterLoader.js +++ b/src/Adapters/AdapterLoader.js @@ -29,7 +29,7 @@ export function loadAdapter(adapter, defaultAdapter, options) { } else if (adapter.adapter) { return loadAdapter(adapter.adapter, undefined, adapter.options); } - // return the adapter as is as it's unusable otherwise + // return the adapter as provided return adapter; } From e074a922fb765e786f06bcc7bb3302d835ddc2e9 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Thu, 3 Mar 2016 09:44:43 -0800 Subject: [PATCH 08/18] Fix leak warnings in tests, use mongodb-runner from node_modules --- package.json | 2 +- src/index.js | 21 ++++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 29e873da..1b6199cf 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "build": "./node_modules/.bin/babel src/ -d lib/", "pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.0.8} ./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", - "posttest": "mongodb-runner stop", + "posttest": "./node_modules/.bin/mongodb-runner stop", "coverage": "cross-env COVERAGE_OPTION='./node_modules/babel-istanbul/lib/cli.js cover -x **/spec/**' npm test", "start": "node ./bin/parse-server", "prepublish": "npm run build" diff --git a/src/index.js b/src/index.js index 076035f8..49769b12 100644 --- a/src/index.js +++ b/src/index.js @@ -232,15 +232,18 @@ function ParseServer({ api.use(middlewares.handleParseErrors); - process.on('uncaughtException', (err) => { - if( err.code === "EADDRINUSE" ) { // user-friendly message for this common error - console.log(`Unable to listen on port ${err.port}. The port is already in use.`); - process.exit(0); - } - else { - throw err; - } - }); + //This causes tests to spew some useless warnings, so disable in test + if (!process.env.TESTING) { + process.on('uncaughtException', (err) => { + if( err.code === "EADDRINUSE" ) { // user-friendly message for this common error + console.log(`Unable to listen on port ${err.port}. The port is already in use.`); + process.exit(0); + } + else { + throw err; + } + }); + } hooksController.load(); return api; From c9f8453171860b5dd0dd3d5073ae42cb1d09ccc4 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Fri, 4 Mar 2016 18:53:37 -0500 Subject: [PATCH 09/18] Fix reversed roles lookup --- spec/ParseRole.spec.js | 65 +++++++++++++++++++++++++++++++++++++----- src/Auth.js | 14 +++++---- 2 files changed, 67 insertions(+), 12 deletions(-) diff --git a/spec/ParseRole.spec.js b/spec/ParseRole.spec.js index 8b4f989f..7f19cd79 100644 --- a/spec/ParseRole.spec.js +++ b/spec/ParseRole.spec.js @@ -1,4 +1,4 @@ - +"use strict"; // Roles are not accessible without the master key, so they are not intended // for use by clients. We can manually test them using the master key. @@ -64,26 +64,30 @@ describe('Parse Role testing', () => { var rolesNames = ["FooRole", "BarRole", "BazRole"]; - var createRole = function(name, parent, user) { + var createRole = function(name, sibling, user) { var role = new Parse.Role(name, new Parse.ACL()); if (user) { var users = role.relation('users'); users.add(user); } - if (parent) { - role.relation('roles').add(parent); + if (sibling) { + role.relation('roles').add(sibling); } return role.save({}, { useMasterKey: true }); } var roleIds = {}; createTestUser().then( (user) => { - - return createRole(rolesNames[0], null, null).then( (aRole) => { + // Put the user on the 1st role + return createRole(rolesNames[0], null, user).then( (aRole) => { roleIds[aRole.get("name")] = aRole.id; + // set the 1st role as a sibling of the second + // user will should have 2 role now return createRole(rolesNames[1], aRole, null); }).then( (anotherRole) => { roleIds[anotherRole.get("name")] = anotherRole.id; - return createRole(rolesNames[2], anotherRole, user); + // set this role as a sibling of the last + // the user should now have 3 roles + return createRole(rolesNames[2], anotherRole, null); }).then( (lastRole) => { roleIds[lastRole.get("name")] = lastRole.id; var auth = new Auth({ config: new Config("test"), isMaster: true, user: user }); @@ -118,6 +122,53 @@ describe('Parse Role testing', () => { }); }); }); + + it("Should properly resolve roles", (done) => { + let admin = new Parse.Role("Admin", new Parse.ACL()); + let moderator = new Parse.Role("Moderator", new Parse.ACL()); + let contentCreator = new Parse.Role('ContentManager', new Parse.ACL()); + + Parse.Object.saveAll([admin, moderator, contentCreator], {useMasterKey: true}).then(() => { + contentCreator.getRoles().add(moderator); + moderator.getRoles().add(admin); + return Parse.Object.saveAll([admin, moderator, contentCreator], {useMasterKey: true}); + }).then(() => { + var auth = new Auth({ config: new Config("test"), isMaster: true }); + // For each role, fetch their sibling, what they inherit + // return with result and roleId for later comparison + let promises = [admin, moderator, contentCreator].map((role) => { + return auth._getAllRoleNamesForId(role.id).then((result) => { + return Parse.Promise.as({ + id: role.id, + roleIds: result + }); + }) + }); + + return Parse.Promise.when(promises); + }).then((results) => { + + results.forEach((result) => { + let id = result.id; + let roleIds = result.roleIds; + if (id == admin.id) { + expect(roleIds.length).toBe(2); + expect(roleIds.indexOf(moderator.id)).not.toBe(-1); + expect(roleIds.indexOf(contentCreator.id)).not.toBe(-1); + } else if (id == moderator.id) { + expect(roleIds.length).toBe(1); + expect(roleIds.indexOf(contentCreator.id)).toBe(0); + } else if (id == contentCreator.id) { + expect(roleIds.length).toBe(0); + } + }); + done(); + }).fail((err) => { + console.error(err); + done(); + }) + + }); }); diff --git a/src/Auth.js b/src/Auth.js index 0b285789..9fae8b35 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -139,18 +139,18 @@ Auth.prototype._loadRoles = function() { }; // Given a role object id, get any other roles it is part of -// TODO: Make recursive to support role nesting beyond 1 level deep Auth.prototype._getAllRoleNamesForId = function(roleID) { + + // As per documentation, a Role inherits AnotherRole + // if this Role is in the roles pointer of this AnotherRole + // Let's find all the roles where this role is in a roles relation var rolePointer = { __type: 'Pointer', className: '_Role', objectId: roleID }; var restWhere = { - '$relatedTo': { - key: 'roles', - object: rolePointer - } + 'roles': rolePointer }; var query = new RestQuery(this.config, master(this.config), '_Role', restWhere, {}); @@ -161,6 +161,10 @@ Auth.prototype._getAllRoleNamesForId = function(roleID) { } var roleIDs = results.map(r => r.objectId); + // we found a list of roles where the roleID + // is referenced in the roles relation, + // Get the roles where those found roles are also + // referenced the same way var parentRolesPromises = roleIDs.map( (roleId) => { return this._getAllRoleNamesForId(roleId); }); From 17bc79b372becbfab526aa719e44a7306dd8f32e Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Fri, 4 Mar 2016 19:40:22 -0500 Subject: [PATCH 10/18] Improves tests, ensure unicity of roleIds --- spec/ParseRole.spec.js | 29 ++++++++++++++++++----------- src/Auth.js | 9 ++------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/spec/ParseRole.spec.js b/spec/ParseRole.spec.js index 7f19cd79..636b8338 100644 --- a/spec/ParseRole.spec.js +++ b/spec/ParseRole.spec.js @@ -126,20 +126,23 @@ describe('Parse Role testing', () => { it("Should properly resolve roles", (done) => { let admin = new Parse.Role("Admin", new Parse.ACL()); let moderator = new Parse.Role("Moderator", new Parse.ACL()); - let contentCreator = new Parse.Role('ContentManager', new Parse.ACL()); - - Parse.Object.saveAll([admin, moderator, contentCreator], {useMasterKey: true}).then(() => { - contentCreator.getRoles().add(moderator); - moderator.getRoles().add(admin); - return Parse.Object.saveAll([admin, moderator, contentCreator], {useMasterKey: true}); + let superModerator = new Parse.Role("SuperModerator", new Parse.ACL()); + let contentManager = new Parse.Role('ContentManager', new Parse.ACL()); + let superContentManager = new Parse.Role('SuperContentManager', new Parse.ACL()); + Parse.Object.saveAll([admin, moderator, contentManager, superModerator, superContentManager], {useMasterKey: true}).then(() => { + contentManager.getRoles().add([moderator, superContentManager]); + moderator.getRoles().add([admin, superModerator]); + superContentManager.getRoles().add(superModerator); + return Parse.Object.saveAll([admin, moderator, contentManager, superModerator, superContentManager], {useMasterKey: true}); }).then(() => { var auth = new Auth({ config: new Config("test"), isMaster: true }); // For each role, fetch their sibling, what they inherit // return with result and roleId for later comparison - let promises = [admin, moderator, contentCreator].map((role) => { + let promises = [admin, moderator, contentManager, superModerator].map((role) => { return auth._getAllRoleNamesForId(role.id).then((result) => { return Parse.Promise.as({ id: role.id, + name: role.get('name'), roleIds: result }); }) @@ -147,19 +150,23 @@ describe('Parse Role testing', () => { return Parse.Promise.when(promises); }).then((results) => { - results.forEach((result) => { let id = result.id; let roleIds = result.roleIds; if (id == admin.id) { expect(roleIds.length).toBe(2); expect(roleIds.indexOf(moderator.id)).not.toBe(-1); - expect(roleIds.indexOf(contentCreator.id)).not.toBe(-1); + expect(roleIds.indexOf(contentManager.id)).not.toBe(-1); } else if (id == moderator.id) { expect(roleIds.length).toBe(1); - expect(roleIds.indexOf(contentCreator.id)).toBe(0); - } else if (id == contentCreator.id) { + expect(roleIds.indexOf(contentManager.id)).toBe(0); + } else if (id == contentManager.id) { expect(roleIds.length).toBe(0); + } else if (id == superModerator.id) { + expect(roleIds.length).toBe(3); + expect(roleIds.indexOf(moderator.id)).not.toBe(-1); + expect(roleIds.indexOf(contentManager.id)).not.toBe(-1); + expect(roleIds.indexOf(superContentManager.id)).not.toBe(-1); } }); done(); diff --git a/src/Auth.js b/src/Auth.js index 9fae8b35..b45f93f3 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -173,14 +173,9 @@ Auth.prototype._getAllRoleNamesForId = function(roleID) { }).then(function(results){ // Flatten let roleIDs = results.reduce( (memo, result) => { - if (typeof result == "object") { - memo = memo.concat(result); - } else { - memo.push(result); - } - return memo; + return memo.concat(result); }, []); - return Promise.resolve(roleIDs); + return Promise.resolve([...new Set(roleIDs)]); }); }; From 7cc059973b30df8c449b51a7bfbe348469aa751b Mon Sep 17 00:00:00 2001 From: Aneesh Devasthale Date: Sat, 5 Mar 2016 10:47:27 +0530 Subject: [PATCH 11/18] Modified the npm dev script to support Windows Windows does not support shebangs/hashbangs. Added the node command to run the bin/dev script. Extension of #831 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1b6199cf..ee0e0b4e 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "nodemon": "^1.8.1" }, "scripts": { - "dev": "npm run build && bin/dev", + "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", "test": "cross-env NODE_ENV=test TESTING=1 ./node_modules/.bin/babel-node $COVERAGE_OPTION ./node_modules/jasmine/bin/jasmine.js", From 4f643d970a947bc1446d4161ad1b90dfe2d4dd36 Mon Sep 17 00:00:00 2001 From: Long Nguyen Date: Sun, 6 Mar 2016 01:32:50 +0700 Subject: [PATCH 12/18] Fix create wrong _Session for Facebook login --- spec/RestCreate.spec.js | 9 +++++++++ src/RestWrite.js | 6 +++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/spec/RestCreate.spec.js b/spec/RestCreate.spec.js index cddfd598..f9b94b37 100644 --- a/spec/RestCreate.spec.js +++ b/spec/RestCreate.spec.js @@ -175,17 +175,26 @@ describe('rest create', () => { } } }; + var newUserSignedUpByFacebookObjectId; rest.create(config, auth.nobody(config), '_User', data) .then((r) => { expect(typeof r.response.objectId).toEqual('string'); expect(typeof r.response.createdAt).toEqual('string'); expect(typeof r.response.sessionToken).toEqual('string'); + newUserSignedUpByFacebookObjectId = r.response.objectId; return rest.create(config, auth.nobody(config), '_User', data); }).then((r) => { expect(typeof r.response.objectId).toEqual('string'); expect(typeof r.response.createdAt).toEqual('string'); expect(typeof r.response.username).toEqual('string'); expect(typeof r.response.updatedAt).toEqual('string'); + expect(r.response.objectId).toEqual(newUserSignedUpByFacebookObjectId); + return rest.find(config, auth.master(config), + '_Session', {sessionToken: r.response.sessionToken}); + }).then((response) => { + expect(response.results.length).toEqual(1); + var output = response.results[0]; + expect(output.user.objectId).toEqual(newUserSignedUpByFacebookObjectId); done(); }); }); diff --git a/src/RestWrite.js b/src/RestWrite.js index a907a61c..d42f2f45 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -178,7 +178,11 @@ RestWrite.prototype.setRequiredFieldsIfNeeded = function() { this.data.updatedAt = this.updatedAt; if (!this.query) { this.data.createdAt = this.updatedAt; - this.data.objectId = cryptoUtils.newObjectId(); + + // Only assign new objectId if we are creating new object + if (!this.data.objectId) { + this.data.objectId = cryptoUtils.newObjectId(); + } } } return Promise.resolve(); From 91eeca3500edab78301004c6481747d209b6c134 Mon Sep 17 00:00:00 2001 From: Igor Shubovych Date: Sun, 6 Mar 2016 00:15:32 +0200 Subject: [PATCH 13/18] Fix Markdown format: make checkboxes visible https://youtu.be/ziSkbxWwDIQ?t=26 --- .github/ISSUE_TEMPLATE.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 0fb61964..10ef133f 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,10 +1,10 @@ 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](https://github.com/ParsePlatform/parse-server/releases) of Parse Server. --[ ] 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=). Chances are that your issue has been reported or resolved before. #### Environment Setup From 4ee9cb754c1693192a3cdebcacfcd8475b2668f3 Mon Sep 17 00:00:00 2001 From: Fosco Marotto Date: Sun, 6 Mar 2016 00:34:44 -0800 Subject: [PATCH 14/18] Fix for related query on non-existing column --- spec/ParseRole.spec.js | 20 ++++++++++++++++++++ src/Controllers/DatabaseController.js | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/spec/ParseRole.spec.js b/spec/ParseRole.spec.js index 8b4f989f..5a9091d2 100644 --- a/spec/ParseRole.spec.js +++ b/spec/ParseRole.spec.js @@ -119,5 +119,25 @@ describe('Parse Role testing', () => { }); }); + it('can create role and query empty users', (done)=> { + var roleACL = new Parse.ACL(); + roleACL.setPublicReadAccess(true); + var role = new Parse.Role('subscribers', roleACL); + role.save({}, {useMasterKey : true}) + .then((x)=>{ + var query = role.relation('users').query(); + query.find({useMasterKey : true}) + .then((users)=>{ + done(); + }, (e)=>{ + fail('should not have errors'); + done(); + }); + }, (e) => { + console.log(e); + fail('should not have errored'); + }); + }); + }); diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 683b9be0..98243acb 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -89,7 +89,7 @@ DatabaseController.prototype.loadSchema = function(acceptor = returnsTrue) { DatabaseController.prototype.redirectClassNameForKey = function(className, key) { return this.loadSchema().then((schema) => { var t = schema.getExpectedType(className, key); - var match = t.match(/^relation<(.*)>$/); + var match = t ? t.match(/^relation<(.*)>$/) : false; if (match) { return match[1]; } else { From 2d4c08c5a3d0e8ee9e44ecb76b52dc2f771dcab5 Mon Sep 17 00:00:00 2001 From: Fosco Marotto Date: Sun, 6 Mar 2016 01:03:51 -0800 Subject: [PATCH 15/18] Test empty authData block on login for #413 --- spec/ParseAPI.spec.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 42ac3491..ef05201c 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -995,4 +995,32 @@ describe('miscellaneous', function() { }); }); + it('android login providing empty authData block works', (done) => { + let headers = { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest' + }; + let data = { + username: 'pulse1989', + password: 'password1234', + authData: {} + }; + let requestOptions = { + headers: headers, + url: 'http://localhost:8378/1/users', + body: JSON.stringify(data) + }; + request.post(requestOptions, (error, response, body) => { + expect(error).toBe(null); + requestOptions.url = 'http://localhost:8378/1/login'; + request.get(requestOptions, (error, response, body) => { + expect(error).toBe(null); + let b = JSON.parse(body); + expect(typeof b['sessionToken']).toEqual('string'); + done(); + }); + }); + }); + }); From 908a4eb6436e5b616c11cce43a605f30cf2c0b28 Mon Sep 17 00:00:00 2001 From: Marco129 Date: Sun, 6 Mar 2016 17:22:36 +0800 Subject: [PATCH 16/18] Fix delete relation field when _Join collection not exist --- spec/Schema.spec.js | 32 ++++++++++++++++++++++++++++++++ src/Schema.js | 6 +++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/spec/Schema.spec.js b/spec/Schema.spec.js index 62ce711e..6ab08d6a 100644 --- a/spec/Schema.spec.js +++ b/spec/Schema.spec.js @@ -577,6 +577,38 @@ describe('Schema', () => { }); }); + it('can delete relation field when related _Join collection not exist', done => { + config.database.loadSchema() + .then(schema => { + schema.addClassIfNotExists('NewClass', { + relationField: {type: 'Relation', targetClass: '_User'} + }) + .then(mongoObj => { + expect(mongoObj).toEqual({ + _id: 'NewClass', + objectId: 'string', + updatedAt: 'string', + createdAt: 'string', + relationField: 'relation<_User>', + }); + }) + .then(() => config.database.collectionExists('_Join:relationField:NewClass')) + .then(exist => { + expect(exist).toEqual(false); + }) + .then(() => schema.deleteField('relationField', 'NewClass', config.database)) + .then(() => schema.reloadData()) + .then(() => { + expect(schema['data']['NewClass']).toEqual({ + objectId: 'string', + updatedAt: 'string', + createdAt: 'string' + }); + done(); + }); + }); + }); + it('can delete string fields and resave as number field', done => { Parse.Object.disableSingleInstance(); var obj1 = hasAllPODobject(); diff --git a/src/Schema.js b/src/Schema.js index 73eaa325..0ed55527 100644 --- a/src/Schema.js +++ b/src/Schema.js @@ -409,7 +409,11 @@ class Schema { if (this.data[className][fieldName].startsWith('relation<')) { //For relations, drop the _Join table - return database.dropCollection(`_Join:${fieldName}:${className}`); + return database.collectionExists(`_Join:${fieldName}:${className}`).then(exist => { + if (exist) { + return database.dropCollection(`_Join:${fieldName}:${className}`); + } + }); } // for non-relations, remove all the data. From 3266d59fccdea86eceb57c8bb2f5426d828e8018 Mon Sep 17 00:00:00 2001 From: Fosco Marotto Date: Sun, 6 Mar 2016 01:56:10 -0800 Subject: [PATCH 17/18] beforeSave changes should propagate to the response --- spec/ParseAPI.spec.js | 17 +++++++++++++++++ src/RestWrite.js | 4 ++++ 2 files changed, 21 insertions(+) diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 42ac3491..2412ac5e 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -967,6 +967,23 @@ 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/src/RestWrite.js b/src/RestWrite.js index d42f2f45..72eda1fc 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -164,6 +164,7 @@ RestWrite.prototype.runBeforeTrigger = function() { }).then((response) => { if (response && response.object) { this.data = response.object; + this.storage['changedByTrigger'] = true; // We should delete the objectId for an update write if (this.query && this.query.objectId) { delete this.data.objectId @@ -806,6 +807,9 @@ RestWrite.prototype.runDatabaseOperation = function() { objectId: this.data.objectId, createdAt: this.data.createdAt }; + if (this.storage['changedByTrigger']) { + Object.assign(resp, this.data); + } if (this.storage['token']) { resp.sessionToken = this.storage['token']; } From 1450795516dd5948a4770398de642db9bb61ad52 Mon Sep 17 00:00:00 2001 From: Fosco Marotto Date: Sun, 6 Mar 2016 03:32:49 -0800 Subject: [PATCH 18/18] Remove limit when counting results. --- src/RestQuery.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/RestQuery.js b/src/RestQuery.js index 9a4764a9..1e0f344e 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -396,6 +396,7 @@ RestQuery.prototype.runCount = function() { } this.findOptions.count = true; delete this.findOptions.skip; + delete this.findOptions.limit; return this.config.database.find( this.className, this.restWhere, this.findOptions).then((c) => { this.response.count = c;