diff --git a/.github/parse-server-logo.png b/.github/parse-server-logo.png
new file mode 100644
index 00000000..80fbc243
Binary files /dev/null and b/.github/parse-server-logo.png differ
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 46a6feba..e7e83371 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/README.md b/README.md
index 19bf9e62..d7ccd2f0 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,4 @@
-
-
-## Parse Server
+
[](https://travis-ci.org/ParsePlatform/parse-server)
[](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
+
+[](https://heroku.com/deploy?template=https://github.com/parseplatform/parse-server-example)
+[](https://azuredeploy.net/?repository=https://github.com/parseplatform/parse-server-example)
+
+
+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`.
diff --git a/package.json b/package.json
index a39b5ca1..1b7eb3c5 100644
--- a/package.json
+++ b/package.json
@@ -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": {
diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js
index 67e414b7..52c17fbf 100644
--- a/spec/ParseAPI.spec.js
+++ b/spec/ParseAPI.spec.js
@@ -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();
+ });
+ });
+
});
diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js
index fd136df4..1a6a3069 100644
--- a/spec/schemas.spec.js
+++ b/spec/schemas.spec.js
@@ -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);
+ });
+ });
});
diff --git a/src/Adapters/Files/S3Adapter.js b/src/Adapters/Files/S3Adapter.js
index dd6c8d0d..0732fbfe 100644
--- a/src/Adapters/Files/S3Adapter.js
+++ b/src/Adapters/Files/S3Adapter.js
@@ -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 } = {}
) {
diff --git a/src/ExportAdapter.js b/src/ExportAdapter.js
index 6587df79..abcc862d 100644
--- a/src/ExportAdapter.js
+++ b/src/ExportAdapter.js
@@ -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.'));
diff --git a/src/Schema.js b/src/Schema.js
index a07018bf..0d601449 100644
--- a/src/Schema.js
+++ b/src/Schema.js
@@ -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,
diff --git a/src/functions.js b/src/functions.js
index c787a814..8e88aa03 100644
--- a/src/functions.js
+++ b/src/functions.js
@@ -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 {
diff --git a/src/middlewares.js b/src/middlewares.js
index 7dcf8889..38d757a9 100644
--- a/src/middlewares.js
+++ b/src/middlewares.js
@@ -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;
diff --git a/src/schemas.js b/src/schemas.js
index cd8b92ec..9fb191c8 100644
--- a/src/schemas.js
+++ b/src/schemas.js
@@ -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;