Merge remote-tracking branch 'upstream/master' into test-configurations

This commit is contained in:
Drew Gross
2016-02-19 12:52:19 -08:00
12 changed files with 260 additions and 71 deletions

BIN
.github/parse-server-logo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -1,5 +1,12 @@
## Parse Server Changelog
### 2.1.1 (2/18/2016)
* Experimental: Schemas API support for DELETE operations
* Fix: Session token issue fetching Users
* Fix: Facebook auth validation
* Fix: Invalid error when deleting missing session
### 2.1.0 (2/17/2016)
* Feature: Support for additional OAuth providers

View File

@@ -1,6 +1,4 @@
<img src="http://parse.com/assets/svgs/parse-infinity.svg" alt="Parse logo" height="70"/>
## Parse Server
![Parse Server logo](.github/parse-server-logo.png?raw=true)
[![Build Status](https://img.shields.io/travis/ParsePlatform/parse-server/master.svg?style=flat)](https://travis-ci.org/ParsePlatform/parse-server)
[![Coverage Status](https://img.shields.io/codecov/c/github/ParsePlatform/parse-server/master.svg)](https://codecov.io/github/ParsePlatform/parse-server?branch=master)
@@ -12,25 +10,50 @@ Parse Server works with the Express web application framework. It can be added t
Read the announcement blog post here: http://blog.parse.com/announcements/introducing-parse-server-and-the-database-migration-tool/
## Getting Started
[![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy?template=https://github.com/parseplatform/parse-server-example)
[![Deploy to Azure](http://azuredeploy.net/deploybutton.png)](https://azuredeploy.net/?repository=https://github.com/parseplatform/parse-server-example)
<a title="Deploy to AWS" href="https://console.aws.amazon.com/elasticbeanstalk/home?region=us-west-2#/newApplication?applicationName=ParseServer&solutionStackName=Node.js&tierName=WebServer&sourceBundleUrl=https://s3.amazonaws.com/elasticbeanstalk-samples-us-east-1/eb-parse-server-sample/parse-server-example.zip" target="_blank"><img src="http://d0.awsstatic.com/product-marketing/Elastic%20Beanstalk/deploy-to-aws.png" height="40"></a>
You can create an instance of ParseServer, and mount it on a new or existing Express website:
```js
var express = require('express');
var ParseServer = require('parse-server').ParseServer;
var app = express();
// Specify the connection string for your mongodb database
// and the location to your Parse cloud code
var api = new ParseServer({
databaseURI: 'mongodb://localhost:27017/dev',
cloud: '/home/myApp/cloud/main.js', // Provide an absolute path
appId: 'myAppId',
masterKey: '', //Add your master key here. Keep it secret!
fileKey: 'optionalFileKey',
serverURL: 'http://localhost:1337/parse' // Don't forget to change to https if needed
});
// Serve the Parse API on the /parse URL prefix
app.use('/parse', api);
app.listen(1337, function() {
console.log('parse-server-example running on port 1337.');
});
```
## Documentation
Documentation for Parse Server is available in the [wiki](https://github.com/ParsePlatform/parse-server/wiki) for this repository. The [Parse Server guide](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide) is a good place to get started.
If you're interested in developing for Parse Server, the [Development guide](https://github.com/ParsePlatform/parse-server/wiki/Development-Guide) will help you get set up.
### Example Project
Check out the [parse-server-example project](https://github.com/ParsePlatform/parse-server-example) repository for an example of a Node.js application that uses the parse-server module on Express.
### Migration Guide
Migrate your existing Parse apps to your own Parse Server. The hosted version of Parse will be fully retired on January 28th, 2017. If you are planning to migrate an app, you need to begin work as soon as possible. Learn more in the [Migration guide](https://github.com/ParsePlatform/parse-server/wiki/Migrating-an-Existing-Parse-App).
The hosted version of Parse will be fully retired on January 28th, 2017. If you are planning to migrate an app, you need to begin work as soon as possible. Learn more in the [Migration guide](https://github.com/ParsePlatform/parse-server/wiki/Migrating-an-Existing-Parse-App).
---
#### Basic options:
* databaseURI (required) - The connection string for your database, i.e. `mongodb://user:pass@host.com/dbname`
@@ -112,46 +135,9 @@ For more informations about custom auth please see the examples:
* databaseAdapter (unfinished) - The backing store can be changed by creating an adapter class (see `DatabaseAdapter.js`)
* loggerAdapter - The default behavior/transport (File) can be changed by creating an adapter class (see [`LoggerAdapter.js`](https://github.com/ParsePlatform/parse-server/blob/master/src/Adapters/Logger/LoggerAdapter.js))
* enableAnonymousUsers - Defaults to true. Set to false to disable anonymous users.
---
### Usage
You can create an instance of ParseServer, and mount it on a new or existing Express website:
```js
var express = require('express');
var ParseServer = require('parse-server').ParseServer;
var app = express();
var port = process.env.PORT || 1337;
// Specify the connection string for your mongodb database
// and the location to your Parse cloud code
var api = new ParseServer({
databaseURI: 'mongodb://localhost:27017/dev',
cloud: '/home/myApp/cloud/main.js', // Provide an absolute path
appId: 'myAppId',
masterKey: '', //Add your master key here. Keep it secret!
fileKey: 'optionalFileKey',
serverURL: 'http://localhost:' + port + '/parse' // Don't forget to change to https if needed
});
// Serve the Parse API on the /parse URL prefix
app.use('/parse', api);
// Hello world
app.get('/', function(req, res) {
res.status(200).send('Express is running here.');
});
app.listen(port, function() {
console.log('parse-server-example running on port ' + port + '.');
});
```
#### Standalone usage
You can configure the Parse Server with environment variables:
@@ -173,8 +159,7 @@ PARSE_SERVER_FACEBOOK_APP_IDS // string of comma separated list
```
Alernatively, you can use the `PARSE_SERVER_OPTIONS` environment variable set to the JSON of your configuration (see Usage).
Alternatively, you can use the `PARSE_SERVER_OPTIONS` environment variable set to the JSON of your configuration (see Usage).
To start the server, just run `npm start`.

View File

@@ -1,6 +1,6 @@
{
"name": "parse-server",
"version": "2.1.0",
"version": "2.1.1",
"description": "An express module providing a Parse-compatible API server",
"main": "lib/index.js",
"repository": {

View File

@@ -587,7 +587,7 @@ describe('miscellaneous', function() {
done();
});
});
it('test cloud function query parameters', (done) => {
Parse.Cloud.define('echoParams', (req, res) => {
res.success(req.params);
@@ -621,8 +621,8 @@ describe('miscellaneous', function() {
// Register a function with validation
Parse.Cloud.define('functionWithParameterValidation', (req, res) => {
res.success('works');
}, (params) => {
return params.success === 100;
}, (request) => {
return request.params.success === 100;
});
Parse.Cloud.run('functionWithParameterValidation', {"success":100}).then((s) => {
@@ -638,8 +638,8 @@ describe('miscellaneous', function() {
// Register a function with validation
Parse.Cloud.define('functionWithParameterValidationFailure', (req, res) => {
res.success('noway');
}, (params) => {
return params.success === 100;
}, (request) => {
return request.params.success === 100;
});
Parse.Cloud.run('functionWithParameterValidationFailure', {"success":500}).then((s) => {
@@ -721,4 +721,15 @@ describe('miscellaneous', function() {
});
});
it('fails on invalid function', done => {
Parse.Cloud.run('somethingThatDoesDefinitelyNotExist').then((s) => {
fail('This should have never suceeded');
done();
}, (e) => {
expect(e.code).toEqual(Parse.Error.SCRIPT_FAILED);
expect(e.message).toEqual('Invalid function.');
done();
});
});
});

View File

@@ -1,6 +1,9 @@
var Parse = require('parse/node').Parse;
var request = require('request');
var dd = require('deep-diff');
var Config = require('../src/Config');
var config = new Config('test');
var hasAllPODobject = () => {
var obj = new Parse.Object('HasAllPOD');
@@ -633,4 +636,102 @@ describe('schemas', () => {
});
});
});
it('requires the master key to delete schemas', done => {
request.del({
url: 'http://localhost:8378/1/schemas/DoesntMatter',
headers: noAuthHeaders,
json: true,
}, (error, response, body) => {
expect(response.statusCode).toEqual(403);
expect(body.error).toEqual('unauthorized');
done();
});
});
it('refuses to delete non-empty collection', done => {
var obj = hasAllPODobject();
obj.save()
.then(() => {
request.del({
url: 'http://localhost:8378/1/schemas/HasAllPOD',
headers: masterKeyHeaders,
json: true,
}, (error, response, body) => {
expect(response.statusCode).toEqual(400);
expect(body.code).toEqual(255);
expect(body.error).toEqual('class HasAllPOD not empty, contains 1 objects, cannot drop schema');
done();
});
});
});
it('fails when deleting collections with invalid class names', done => {
request.del({
url: 'http://localhost:8378/1/schemas/_GlobalConfig',
headers: masterKeyHeaders,
json: true,
}, (error, response, body) => {
expect(response.statusCode).toEqual(400);
expect(body.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
expect(body.error).toEqual('Invalid classname: _GlobalConfig, classnames can only have alphanumeric characters and _, and must start with an alpha character ');
done();
})
});
it('does not fail when deleting nonexistant collections', done => {
request.del({
url: 'http://localhost:8378/1/schemas/Missing',
headers: masterKeyHeaders,
json: true,
}, (error, response, body) => {
expect(response.statusCode).toEqual(200);
expect(body).toEqual({});
done();
});
});
it('deletes collections including join tables', done => {
var obj = new Parse.Object('MyClass');
obj.set('data', 'data');
obj.save()
.then(() => {
var obj2 = new Parse.Object('MyOtherClass');
var relation = obj2.relation('aRelation');
relation.add(obj);
return obj2.save();
})
.then(obj2 => obj2.destroy())
.then(() => {
request.del({
url: 'http://localhost:8378/1/schemas/MyOtherClass',
headers: masterKeyHeaders,
json: true,
}, (error, response, body) => {
expect(response.statusCode).toEqual(200);
expect(response.body).toEqual({});
config.database.db.collection('test__Join:aRelation:MyOtherClass', { strict: true }, (err, coll) => {
//Expect Join table to be gone
expect(err).not.toEqual(null);
config.database.db.collection('test_MyOtherClass', { strict: true }, (err, coll) => {
// Expect data table to be gone
expect(err).not.toEqual(null);
request.get({
url: 'http://localhost:8378/1/schemas/MyOtherClass',
headers: masterKeyHeaders,
json: true,
}, (error, response, body) => {
//Expect _SCHEMA entry to be gone.
expect(response.statusCode).toEqual(400);
expect(body.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
expect(body.error).toEqual('class MyOtherClass does not exist');
done();
});
});
});
});
}, error => {
fail(error);
});
});
});

View File

@@ -6,7 +6,6 @@ import * as AWS from 'aws-sdk';
import { FilesAdapter } from './FilesAdapter';
const DEFAULT_S3_REGION = "us-east-1";
const DEFAULT_S3_BUCKET = "parse-files";
export class S3Adapter extends FilesAdapter {
// Creates an S3 session.
@@ -15,8 +14,8 @@ export class S3Adapter extends FilesAdapter {
constructor(
accessKey,
secretKey,
bucket,
{ region = DEFAULT_S3_REGION,
bucket = DEFAULT_S3_BUCKET,
bucketPrefix = '',
directAccess = false } = {}
) {

View File

@@ -306,7 +306,8 @@ ExportAdapter.prototype.destroy = function(className, query, options = {}) {
return coll.remove(mongoWhere);
}).then((resp) => {
if (resp.result.n === 0) {
//Check _Session to avoid changing password failed without any session.
if (resp.result.n === 0 && className !== "_Session") {
return Promise.reject(
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
'Object not found.'));

View File

@@ -521,7 +521,7 @@ Schema.prototype.deleteField = function(fieldName, className, database, prefix)
});
}
if (schema.data[className][fieldName].startsWith('relation')) {
if (schema.data[className][fieldName].startsWith('relation<')) {
//For relations, drop the _Join table
return database.dropCollection(prefix + '_Join:' + fieldName + ':' + className)
//Save the _SCHEMA object
@@ -714,6 +714,7 @@ function getObjectType(obj) {
module.exports = {
load: load,
classNameIsValid: classNameIsValid,
invalidClassNameMessage: invalidClassNameMessage,
mongoSchemaFromFieldsAndClassName: mongoSchemaFromFieldsAndClassName,
schemaAPITypeToMongoFieldType: schemaAPITypeToMongoFieldType,
buildMergedSchemaObject: buildMergedSchemaObject,

View File

@@ -10,10 +10,15 @@ var router = new PromiseRouter();
function handleCloudFunction(req) {
if (Parse.Cloud.Functions[req.params.functionName]) {
const params = Object.assign({}, req.body, req.query);
var request = {
params: Object.assign({}, req.body, req.query),
master: req.auth && req.auth.isMaster,
user: req.auth && req.auth.user,
installationId: req.info.installationId
};
if (Parse.Cloud.Validators[req.params.functionName]) {
var result = Parse.Cloud.Validators[req.params.functionName](params);
var result = Parse.Cloud.Validators[req.params.functionName](request);
if (!result) {
throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Validation failed.');
}
@@ -21,12 +26,6 @@ function handleCloudFunction(req) {
return new Promise(function (resolve, reject) {
var response = createResponseObject(resolve, reject);
var request = {
params: params,
master: req.auth && req.auth.isMaster,
user: req.auth && req.auth.user,
installationId: req.info.installationId
};
Parse.Cloud.Functions[req.params.functionName](request, response);
});
} else {

View File

@@ -26,7 +26,7 @@ function handleParseHeaders(req, res, next) {
restAPIKey: req.get('X-Parse-REST-API-Key')
};
if (req.body && req.body._noBody) {
if (req.body) {
// Unity SDK sends a _noBody key which needs to be removed.
// Unclear at this point if action needs to be taken.
delete req.body._noBody;

View File

@@ -183,10 +183,95 @@ function modifySchema(req) {
});
}
// A helper function that removes all join tables for a schema. Returns a promise.
var removeJoinTables = (database, prefix, mongoSchema) => {
return Promise.all(Object.keys(mongoSchema)
.filter(field => mongoSchema[field].startsWith('relation<'))
.map(field => {
var joinCollectionName = prefix + '_Join:' + field + ':' + mongoSchema._id;
return new Promise((resolve, reject) => {
database.dropCollection(joinCollectionName, (err, results) => {
if (err) {
reject(err);
} else {
resolve();
}
})
});
})
);
};
function deleteSchema(req) {
if (!req.auth.isMaster) {
return masterKeyRequiredResponse();
}
if (!Schema.classNameIsValid(req.params.className)) {
return Promise.resolve({
status: 400,
response: {
code: Parse.Error.INVALID_CLASS_NAME,
error: Schema.invalidClassNameMessage(req.params.className),
}
});
}
return req.config.database.collection(req.params.className)
.then(coll => new Promise((resolve, reject) => {
coll.count((err, count) => {
if (err) {
reject(err);
} else if (count > 0) {
resolve({
status: 400,
response: {
code: 255,
error: 'class ' + req.params.className + ' not empty, contains ' + count + ' objects, cannot drop schema',
}
});
} else {
coll.drop((err, reply) => {
if (err) {
reject(err);
} else {
// We've dropped the collection now, so delete the item from _SCHEMA
// and clear the _Join collections
req.config.database.collection('_SCHEMA')
.then(coll => new Promise((resolve, reject) => {
coll.findAndRemove({ _id: req.params.className }, [], (err, doc) => {
if (err) {
reject(err);
} else if (doc.value === null) {
//tried to delete non-existant class
resolve({ response: {}});
} else {
removeJoinTables(req.config.database.db, req.config.database.collectionPrefix, doc.value)
.then(resolve, reject);
}
});
}))
.then(resolve.bind(undefined, {response: {}}), reject);
}
});
}
});
}))
.catch(error => {
if (error.message == 'ns not found') {
// If they try to delete a non-existant class, thats fine, just let them.
return Promise.resolve({ response: {} });
} else {
return Promise.reject(error);
}
});
}
router.route('GET', '/schemas', getAllSchemas);
router.route('GET', '/schemas/:className', getOneSchema);
router.route('POST', '/schemas', createSchema);
router.route('POST', '/schemas/:className', createSchema);
router.route('PUT', '/schemas/:className', modifySchema);
router.route('DELETE', '/schemas/:className', deleteSchema);
module.exports = router;