Compare commits

...
13 Commits
Author SHA1 Message Date
Drew Gross b40e16647b Changelog for version 2.2.10 2016-05-16 16:39:41 -07:00
Tyler Brock 3b4ae2d0a0 Write old ACL format in _acl in addition to new format (#1810) 2016-05-16 14:41:25 -07:00
Tyler Brock 1854928fe7 Add test to ensure you can set ACL in beforeSave (#1772) 2016-05-13 18:17:22 -07:00
Drew e4998c256a Move field name validation logic out of mongo (#1752)
* Remove transformKey(...)

* Move validation logic into Parse Server and out of Mongo Adapter

* Fix nits
2016-05-13 15:28:14 -07:00
Peter J. Shin 4bfe2c5014 Merge pull request #1774 from drew-gross/pointer-perms-in-dash
Tell the dashboard that Pointer Permissions are implemented so it can let you edit them.
2016-05-13 14:47:24 -07:00
Marco Cheung d0c3535a39 Fix error when unset user email (#1778) 2016-05-13 10:51:01 -07:00
Drew Gross c2cfa14627 Tell dashboard that the feature exits 2016-05-12 16:30:38 -07:00
Marco Cheung 19e7407f55 Return correct error when violating unique index (#1763) 2016-05-11 17:24:15 -07:00
North 6cfcb4ddf8 Fix #1755 (#1756)
* Add condition at limit = 0

* Add tests for installations with limit and count parameters
2016-05-11 15:22:29 -07:00
Drew 10ee229e60 Update dependencies and support node 6.1 (#1754)
* Update dependencies and support node 6.1

* Remove gcs-adapter, which depends on grpc, which fails to build on node 6

* Remove GCS adapter usage
2016-05-11 08:54:23 -04:00
Sebastián Balay 1291f70a16 Fix malformed config.json in bootstrap.sh
The bootstrap bash script generates a wrong config.json file.
Instead of having a databaseURI property, it has a mongodbURI prop.
2016-05-10 18:13:06 -04:00
Drew 26df50924b Update to 2.2.9 (#1747) 2016-05-10 10:50:05 -07:00
Drew e698f61a58 Handle null param in cloud code. Fixes #1472 (#1746) 2016-05-10 10:43:02 -07:00
17 changed files with 258 additions and 53 deletions
+1
View File
@@ -1,6 +1,7 @@
language: node_js
node_js:
- '4.3'
- '6.1'
env:
global:
- COVERAGE_OPTION='./node_modules/babel-istanbul/lib/cli.js cover -x **/spec/**'
+12
View File
@@ -1,5 +1,17 @@
## Parse Server Changelog
### 2.2.10 (5/15/2016)
* Fix: Write legacy ACLs to Mongo so that clients that still go through Parse.com can read them, thanks to [Tyler Brock](https://github.com/TylerBrock) and [carmenlau](https://github.com/carmenlau)
* Fix: Querying installations with limit = 0 and count = 1 now works, thanks to [ssk7833](https://github.com/ssk7833)
* Fix: Return correct error when violating unique index, thanks to [Marco Cheung](https://github.com/Marco129)
* Fix: Allow unsetting user's email, thanks to [Marco Cheung](https://github.com/Marco129)
* New: Support for Node 6.1
### 2.2.9 (5/9/2016)
* Fix: Fix a regression that caused Parse Server to crash when a null parameter is passed to a Cloud function
### 2.2.8 (5/8/2016)
* New: Support for Pointer Permissions
+1 -1
View File
@@ -103,7 +103,7 @@ cat > ./config.json << EOF
"masterKey": "$MASTER_KEY",
"appName": "$APP_NAME",
"cloud": "./cloud/main",
"mongodbURI": "$MONGODB_URI"
"databaseURI": "$MONGODB_URI"
}
EOF
echo "${CHECK} Created config.json"
+4 -6
View File
@@ -1,6 +1,6 @@
{
"name": "parse-server",
"version": "2.2.8",
"version": "2.2.10",
"description": "An express module providing a Parse-compatible API server",
"main": "lib/index.js",
"repository": {
@@ -57,12 +57,10 @@
"codecov": "^1.0.1",
"cross-env": "^1.0.7",
"deep-diff": "^0.3.3",
"flow-bin": "^0.22.0",
"gaze": "^0.5.2",
"gaze": "^1.0.0",
"jasmine": "^2.3.2",
"mongodb-runner": "3.1.15",
"nodemon": "^1.8.1",
"parse-server-gcs-adapter": "^1.0.0"
"mongodb-runner": "3.2.2",
"nodemon": "^1.8.1"
},
"scripts": {
"dev": "npm run build && node bin/dev",
-10
View File
@@ -2,7 +2,6 @@
var loadAdapter = require("../src/Adapters/AdapterLoader").loadAdapter;
var FilesAdapter = require("parse-server-fs-adapter").default;
var S3Adapter = require("parse-server-s3-adapter").default;
var GCSAdapter = require("parse-server-gcs-adapter").default;
var ParsePushAdapter = require("parse-server-push-adapter").default;
describe("AdapterLoader", ()=>{
@@ -114,13 +113,4 @@ describe("AdapterLoader", ()=>{
}).not.toThrow();
done();
})
it("should load GCSAdapter from direct passing", (done) => {
var gcsAdapter = new GCSAdapter("projectId", "path/to/keyfile", "bucket")
expect(() => {
var adapter = loadAdapter(gcsAdapter, FilesAdapter);
expect(adapter).toBe(gcsAdapter);
}).not.toThrow();
done();
})
});
+96
View File
@@ -71,4 +71,100 @@ describe('InstallationsRouter', () => {
done();
});
});
it('query installations with limit = 0', (done) => {
var androidDeviceRequest = {
'installationId': '12345678-abcd-abcd-abcd-123456789abc',
'deviceType': 'android'
};
var iosDeviceRequest = {
'installationId': '12345678-abcd-abcd-abcd-123456789abd',
'deviceType': 'ios'
};
var request = {
config: config,
auth: auth.master(config),
body: {},
query: {
limit: 0
}
};
var router = new InstallationsRouter();
rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest)
.then(() => {
return rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest);
}).then(() => {
return router.handleFind(request);
}).then((res) => {
var response = res.response;
expect(response.results.length).toEqual(0);
done();
});
});
it('query installations with count = 1', (done) => {
var androidDeviceRequest = {
'installationId': '12345678-abcd-abcd-abcd-123456789abc',
'deviceType': 'android'
};
var iosDeviceRequest = {
'installationId': '12345678-abcd-abcd-abcd-123456789abd',
'deviceType': 'ios'
};
var request = {
config: config,
auth: auth.master(config),
body: {},
query: {
count: 1
}
};
var router = new InstallationsRouter();
rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest)
.then(() => {
return rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest);
}).then(() => {
return router.handleFind(request);
}).then((res) => {
var response = res.response;
expect(response.results.length).toEqual(2);
expect(response.count).toEqual(2);
done();
});
});
it('query installations with limit = 0 and count = 1', (done) => {
var androidDeviceRequest = {
'installationId': '12345678-abcd-abcd-abcd-123456789abc',
'deviceType': 'android'
};
var iosDeviceRequest = {
'installationId': '12345678-abcd-abcd-abcd-123456789abd',
'deviceType': 'ios'
};
var request = {
config: config,
auth: auth.master(config),
body: {},
query: {
limit: 0,
count: 1
}
};
var router = new InstallationsRouter();
rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest)
.then(() => {
return rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest);
}).then(() => {
return router.handleFind(request);
}).then((res) => {
var response = res.response;
expect(response.results.length).toEqual(0);
expect(response.count).toEqual(2);
done();
});
});
});
+15 -11
View File
@@ -191,17 +191,6 @@ describe('untransformObject', () => {
});
});
describe('transformKey', () => {
it('throws out _password', (done) => {
try {
transform.transformKey(dummySchema, '_User', '_password');
fail('should have thrown');
} catch (e) {
done();
}
});
});
describe('transform schema key changes', () => {
it('changes new pointer key', (done) => {
@@ -244,6 +233,21 @@ describe('transform schema key changes', () => {
done();
});
it('writes the old ACL format in addition to rperm and wperm', (done) => {
var input = {
ACL: {
"*": { "read": true },
"Kevin": { "write": true }
}
};
var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input);
expect(typeof output._acl).toEqual('object');
expect(output._acl["Kevin"].w).toBeTruthy();
expect(output._acl["Kevin"].r).toBeUndefined();
done();
})
it('untransforms from _rperm and _wperm to ACL', (done) => {
var input = {
_rperm: ["*"],
+66
View File
@@ -5,6 +5,7 @@
var DatabaseAdapter = require('../src/DatabaseAdapter');
var request = require('request');
const Parse = require("parse/node");
let Config = require('../src/Config');
describe('miscellaneous', function() {
it('create a GameScore object', function(done) {
@@ -224,6 +225,34 @@ describe('miscellaneous', function() {
});
});
it('test beforeSave set object acl success', function(done) {
var acl = new Parse.ACL({
'*': { read: true, write: false }
});
Parse.Cloud.beforeSave('BeforeSaveAddACL', function(req, res) {
req.object.setACL(acl);
res.success();
});
var obj = new Parse.Object('BeforeSaveAddACL');
obj.set('lol', true);
obj.save().then(function() {
Parse.Cloud._removeHook('Triggers', 'beforeSave', 'BeforeSaveAddACL');
var query = new Parse.Query('BeforeSaveAddACL');
query.get(obj.id).then(function(objAgain) {
expect(objAgain.get('lol')).toBeTruthy();
expect(objAgain.getACL().equals(acl));
done();
}, function(error) {
fail(error);
done();
});
}, function(error) {
fail(error);
done();
});
});
it('test beforeSave returns value on create and update', (done) => {
var obj = new Parse.Object('BeforeSaveChanged');
obj.set('foo', 'bing');
@@ -1119,6 +1148,22 @@ describe('miscellaneous', function() {
});
});
it('can handle null params in cloud functions (regression test for #1742)', done => {
Parse.Cloud.define('func', (request, response) => {
expect(request.params.nullParam).toEqual(null);
response.success('yay');
});
Parse.Cloud.run('func', {nullParam: null})
.then(() => {
Parse.Cloud._removeHook('Functions', 'func');
done()
}, e => {
fail('cloud code call failed');
done();
});
});
it('fails on invalid client key', done => {
var headers = {
'Content-Type': 'application/octet-stream',
@@ -1371,4 +1416,25 @@ describe('miscellaneous', function() {
})
});
});
it('fail when create duplicate value in unique field', (done) => {
let obj = new Parse.Object('UniqueField');
obj.set('unique', 'value');
obj.save().then(() => {
expect(obj.id).not.toBeUndefined();
let config = new Config('test');
return config.database.adapter.adaptiveCollection('UniqueField')
}).then(collection => {
return collection._mongoCollection.createIndex({ 'unique': 1 }, { unique: true })
}).then(() => {
let obj = new Parse.Object('UniqueField');
obj.set('unique', 'value');
return obj.save()
}).then(() => {
return Promise.reject();
}, error => {
expect(error.code === Parse.Error.DUPLICATE_VALUE);
done();
});
});
});
+16
View File
@@ -1834,6 +1834,22 @@ describe('Parse.User testing', () => {
});
});
it('unset user email', (done) => {
var user = new Parse.User();
user.set('username', 'test');
user.set('password', 'test');
user.set('email', 'test@test.com');
user.signUp().then(() => {
user.unset('email');
return user.save();
}).then(() => {
return Parse.User.logIn('test', 'test');
}).then((user) => {
expect(user.getEmail()).toBeUndefined();
done();
});
});
it('create session from user', (done) => {
Parse.Promise.as().then(() => {
return Parse.User.signUp("finn", "human", { foo: "bar" });
@@ -166,7 +166,14 @@ export class MongoStorageAdapter {
createObject(className, object, schemaController, parseFormatSchema) {
const mongoObject = transform.parseObjectToMongoObjectForCreate(schemaController, className, object, parseFormatSchema);
return this.adaptiveCollection(className)
.then(collection => collection.insertOne(mongoObject));
.then(collection => collection.insertOne(mongoObject))
.catch(error => {
if (error.code === 11000) { // Duplicate value
throw new Parse.Error(Parse.Error.DUPLICATE_VALUE,
'A duplicate value for a field with unique values was provided');
}
return Promise.reject(error);
});
}
// Remove all objects that match the given parse query. Parse Query should be in Parse Format.
+12 -13
View File
@@ -15,14 +15,11 @@ var Parse = require('parse/node').Parse;
// in the value are converted to a mongo update form. Otherwise they are
// converted to static data.
//
// validate: true indicates that key names are to be validated.
//
// Returns an object with {key: key, value: value}.
function transformKeyValue(schema, className, restKey, restValue, {
inArray,
inObject,
update,
validate,
} = {}) {
// Check if the schema is known since it's a built-in field.
var key = restKey;
@@ -71,9 +68,6 @@ function transformKeyValue(schema, className, restKey, restValue, {
if (authDataMatch) {
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'can only query on ' + key);
}
if (validate && !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) {
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'invalid key name: ' + key);
}
}
// Handle special schema key changes
@@ -354,7 +348,7 @@ function transformUpdate(schema, className, restUpdate) {
var mongoUpdate = {};
var acl = transformACL(restUpdate);
if (acl._rperm || acl._wperm) {
if (acl._rperm || acl._wperm || acl._acl) {
mongoUpdate['$set'] = {};
if (acl._rperm) {
mongoUpdate['$set']['_rperm'] = acl._rperm;
@@ -362,6 +356,9 @@ function transformUpdate(schema, className, restUpdate) {
if (acl._wperm) {
mongoUpdate['$set']['_wperm'] = acl._wperm;
}
if (acl._acl) {
mongoUpdate['$set']['_acl'] = acl._acl;
}
}
for (var restKey in restUpdate) {
@@ -410,16 +407,23 @@ function transformACL(restObject) {
var acl = restObject['ACL'];
var rperm = [];
var wperm = [];
var _acl = {}; // old format
for (var entry in acl) {
if (acl[entry].read) {
rperm.push(entry);
_acl[entry] = _acl[entry] || {};
_acl[entry]['r'] = true;
}
if (acl[entry].write) {
wperm.push(entry);
_acl[entry] = _acl[entry] || {};
_acl[entry]['w'] = true;
}
}
output._rperm = rperm;
output._wperm = wperm;
output._acl = _acl;
delete restObject.ACL;
return output;
}
@@ -454,11 +458,6 @@ function untransformACL(mongoObject) {
return output;
}
// Transforms a key used in the REST API format to its mongo format.
function transformKey(schema, className, key) {
return transformKeyValue(schema, className, key, null, {validate: true}).key;
}
// A sentinel value that helper transformations return when they
// cannot perform a transformation
function CannotTransform() {}
@@ -1038,7 +1037,7 @@ var FileCoder = {
};
module.exports = {
transformKey,
transformKeyValue,
parseObjectToMongoObjectForCreate,
transformUpdate,
transformWhere,
+16 -3
View File
@@ -618,9 +618,22 @@ DatabaseController.prototype.find = function(className, query, {
.then(schemaController => {
if (sort) {
mongoOptions.sort = {};
for (let key in sort) {
let mongoKey = this.transform.transformKey(schemaController, className, key);
mongoOptions.sort[mongoKey] = sort[key];
for (let fieldName in sort) {
// Parse.com treats queries on _created_at and _updated_at as if they were queries on createdAt and updatedAt,
// so duplicate that behaviour here.
if (fieldName === '_created_at') {
fieldName = 'createdAt';
sort['createdAt'] = sort['_created_at'];
} else if (fieldName === '_updated_at') {
fieldName = 'updatedAt';
sort['updatedAt'] = sort['_updated_at'];
}
if (!SchemaController.fieldNameIsValid(fieldName)) {
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name: ${fieldName}.`);
}
const mongoKey = this.transform.transformKeyValue(schemaController, className, fieldName, null).key;
mongoOptions.sort[mongoKey] = sort[fieldName];
}
}
return (isMaster ? Promise.resolve() : schemaController.validatePermission(className, aclGroup, op))
+7 -5
View File
@@ -253,7 +253,7 @@ class SchemaController {
this.data[schema.className] = schema.fields;
this.perms[schema.className] = schema.classLevelPermissions;
});
// Inject the in-memory classes
volatileClasses.forEach(className => {
this.data[className] = injectDefaultSchema({
@@ -466,15 +466,16 @@ class SchemaController {
// If 'freeze' is true, refuse to update the schema for this field.
validateField(className, fieldName, type, freeze) {
return this.reloadData().then(() => {
// Just to check that the fieldName is valid
this._collection.transform.transformKey(this, className, fieldName);
if( fieldName.indexOf(".") > 0 ) {
if (fieldName.indexOf(".") > 0) {
// subdocument key (x.y) => ok if x is of type 'object'
fieldName = fieldName.split(".")[ 0 ];
type = 'Object';
}
if (!fieldNameIsValid(fieldName)) {
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name: ${fieldName}.`);
}
let expected = this.data[className][fieldName];
if (expected) {
expected = (expected === 'map' ? 'Object' : expected);
@@ -847,6 +848,7 @@ function getObjectType(obj) {
export {
load,
classNameIsValid,
fieldNameIsValid,
invalidClassNameMessage,
buildMergedSchemaObject,
systemClasses,
+1 -1
View File
@@ -356,7 +356,7 @@ RestWrite.prototype.transformUser = function() {
return Promise.resolve();
});
}).then(() => {
if (!this.data.email) {
if (!this.data.email || this.data.email.__op === 'Delete') {
return;
}
// Validate basic email address format
+1
View File
@@ -39,6 +39,7 @@ export class FeaturesRouter extends PromiseRouter {
clearAllDataFromClass: false,
exportClass: false,
editClassLevelPermissions: true,
editPointerPermissions: true,
},
};
+1 -1
View File
@@ -36,7 +36,7 @@ export class FunctionsRouter extends PromiseRouter {
for (var key in params) {
if (params.hasOwnProperty(key)) {
var value = params[key];
if (value.__type == 'Date') {
if (value && value.__type == 'Date') {
params[key] = new Date(value.iso);
}
}
+1 -1
View File
@@ -12,7 +12,7 @@ export class InstallationsRouter extends ClassesRouter {
if (body.skip) {
options.skip = Number(body.skip);
}
if (body.limit) {
if (body.limit || body.limit === 0) {
options.limit = Number(body.limit);
}
if (body.order) {