mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b40e16647b | ||
|
|
3b4ae2d0a0 | ||
|
|
1854928fe7 | ||
|
|
e4998c256a | ||
|
|
4bfe2c5014 | ||
|
|
d0c3535a39 | ||
|
|
c2cfa14627 | ||
|
|
19e7407f55 | ||
|
|
6cfcb4ddf8 | ||
|
|
10ee229e60 | ||
|
|
1291f70a16 | ||
|
|
26df50924b | ||
|
|
e698f61a58 |
@@ -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/**'
|
||||
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
})
|
||||
});
|
||||
|
||||
@@ -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
@@ -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: ["*"],
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -39,6 +39,7 @@ export class FeaturesRouter extends PromiseRouter {
|
||||
clearAllDataFromClass: false,
|
||||
exportClass: false,
|
||||
editClassLevelPermissions: true,
|
||||
editPointerPermissions: true,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user