Merge branch 'master' of https://github.com/ParsePlatform/parse-server into mcdonald-gcs-adapter

* 'master' of https://github.com/ParsePlatform/parse-server:
  Remove limit when counting results.
  beforeSave changes should propagate to the response
  Fix delete relation field when _Join collection not exist
  Test empty authData block on login for #413
  Fix for related query on non-existing column
  Fix Markdown format: make checkboxes visible
  Fix create wrong _Session for Facebook login
  Modified the npm dev script to support Windows
  Improves tests, ensure unicity of roleIds
  Fix reversed roles lookup
  Fix leak warnings in tests, use mongodb-runner from node_modules
  Improves documentation, add loading tests
  Improves loading of Push Adapter, fix loading of S3Adapter
  Adds public_html and views for packaging
  Removes shebang for windows
  Better support for windows builds
  Fix add field to system schema
  Convert Schema.js to ES6 class.
This commit is contained in:
Mike McDonald
2016-03-06 15:34:40 -08:00
20 changed files with 740 additions and 469 deletions

View File

@@ -28,15 +28,8 @@ 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 the adapter as provided
return adapter;
}

View File

@@ -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;

View File

@@ -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);
});
@@ -169,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)]);
});
};

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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
@@ -178,7 +179,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();
@@ -802,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'];
}

View File

@@ -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') {

View File

@@ -71,7 +71,6 @@ var defaultColumns = {
}
};
var requiredColumns = {
_Product: ["productIdentifier", "icon", "order", "title", "subtitle"],
_Role: ["name", "ACL"]
@@ -168,54 +167,380 @@ 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.collectionExists(`_Join:${fieldName}:${className}`).then(exist => {
if (exist) {
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 +656,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 +664,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 +672,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 +680,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 +736,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,

View File

@@ -134,7 +134,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,
@@ -233,15 +234,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;