Merge remote-tracking branch 'ParsePlatform/master' into user-roles
This commit is contained in:
BIN
.github/parse-server-logo.png
vendored
Normal file
BIN
.github/parse-server-logo.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
14
README.md
14
README.md
@@ -1,6 +1,4 @@
|
||||
<img src="http://parse.com/assets/svgs/parse-infinity.svg" alt="Parse logo" height="70"/>
|
||||
|
||||
## Parse Server
|
||||

|
||||
|
||||
[](https://travis-ci.org/ParsePlatform/parse-server)
|
||||
[](https://codecov.io/github/ParsePlatform/parse-server?branch=master)
|
||||
@@ -18,14 +16,9 @@ Documentation for Parse Server is available in the [wiki](https://github.com/Par
|
||||
|
||||
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).
|
||||
|
||||
|
||||
---
|
||||
@@ -112,6 +105,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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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.'));
|
||||
|
||||
@@ -54,10 +54,17 @@ export class ClassesRouter {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.');
|
||||
}
|
||||
|
||||
if(req.params.className === "_User"){
|
||||
if (req.params.className === "_User") {
|
||||
|
||||
delete response.results[0].sessionToken;
|
||||
}
|
||||
|
||||
|
||||
const user = response.results[0];
|
||||
|
||||
if (req.auth.user && user.objectId == req.auth.user.id) {
|
||||
// Force the session token
|
||||
response.results[0].sessionToken = req.info.sessionToken;
|
||||
}
|
||||
}
|
||||
return { response: response.results[0] };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
// Helper functions for accessing the Facebook Graph API.
|
||||
var https = require('https');
|
||||
var Parse = require('parse/node').Parse;
|
||||
|
||||
// Returns a promise that fulfills iff this user id is valid.
|
||||
function validateAuthData(authData) {
|
||||
return graphRequest('me?fields=id&access_token=' + authData.access_token)
|
||||
.then((data) => {
|
||||
if (data && data.id == authData.id) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Facebook auth is invalid for this user.');
|
||||
});
|
||||
}
|
||||
|
||||
// Returns a promise that fulfills iff this app id is valid.
|
||||
function validateAppId(appIds, authData) {
|
||||
var access_token = authData.access_token;
|
||||
if (!appIds.length) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Facebook auth is not configured.');
|
||||
}
|
||||
return graphRequest('app?access_token=' + access_token)
|
||||
.then((data) => {
|
||||
if (data && appIds.indexOf(data.id) != -1) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Facebook auth is invalid for this user.');
|
||||
});
|
||||
}
|
||||
|
||||
// A promisey wrapper for FB graph requests.
|
||||
function graphRequest(path) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
https.get('https://graph.facebook.com/v2.5/' + path, function(res) {
|
||||
var data = '';
|
||||
res.on('data', function(chunk) {
|
||||
data += chunk;
|
||||
});
|
||||
res.on('end', function() {
|
||||
data = JSON.parse(data);
|
||||
resolve(data);
|
||||
});
|
||||
}).on('error', function(e) {
|
||||
reject('Failed to validate this access token with Facebook.');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validateAppId: validateAppId,
|
||||
validateAuthData: validateAuthData
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
@@ -16,7 +16,8 @@ function validateAuthData(authData) {
|
||||
}
|
||||
|
||||
// Returns a promise that fulfills iff this app id is valid.
|
||||
function validateAppId(appIds, access_token) {
|
||||
function validateAppId(appIds, authData) {
|
||||
var access_token = authData.access_token;
|
||||
if (!appIds.length) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user