Merge branch 'master' of https://github.com/ParsePlatform/parse-server into mcdonald-gcs-adapter
* 'master' of https://github.com/ParsePlatform/parse-server: Remove limit when counting results. beforeSave changes should propagate to the response Fix delete relation field when _Join collection not exist Test empty authData block on login for #413 Fix for related query on non-existing column Fix Markdown format: make checkboxes visible Fix create wrong _Session for Facebook login Modified the npm dev script to support Windows Improves tests, ensure unicity of roleIds Fix reversed roles lookup Fix leak warnings in tests, use mongodb-runner from node_modules Improves documentation, add loading tests Improves loading of Push Adapter, fix loading of S3Adapter Adds public_html and views for packaging Removes shebang for windows Better support for windows builds Fix add field to system schema Convert Schema.js to ES6 class.
This commit is contained in:
6
.github/ISSUE_TEMPLATE.md
vendored
6
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,10 +1,10 @@
|
|||||||
Make sure these boxes are checked before submitting your issue -- thanks for reporting issues back to Parse Server!
|
Make sure these boxes are checked before submitting your issue -- thanks for reporting issues back to Parse Server!
|
||||||
|
|
||||||
-[ ] You've met the [prerequisites](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#prerequisites).
|
- [ ] You've met the [prerequisites](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#prerequisites).
|
||||||
|
|
||||||
-[ ] You're running the [latest version](https://github.com/ParsePlatform/parse-server/releases) of Parse Server.
|
- [ ] You're running the [latest version](https://github.com/ParsePlatform/parse-server/releases) of Parse Server.
|
||||||
|
|
||||||
-[ ] You've searched through [existing issues](https://github.com/ParsePlatform/parse-server/issues?utf8=%E2%9C%93&q=). Chances are that your issue has been reported or resolved before.
|
- [ ] You've searched through [existing issues](https://github.com/ParsePlatform/parse-server/issues?utf8=%E2%9C%93&q=). Chances are that your issue has been reported or resolved before.
|
||||||
|
|
||||||
#### Environment Setup
|
#### Environment Setup
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ node_js:
|
|||||||
- "4.3"
|
- "4.3"
|
||||||
env:
|
env:
|
||||||
global:
|
global:
|
||||||
- CODE_COVERAGE=1
|
- COVERAGE_OPTION='./node_modules/babel-istanbul/lib/cli.js cover -x **/spec/**'
|
||||||
matrix:
|
matrix:
|
||||||
- MONGODB_VERSION=2.6.11
|
- MONGODB_VERSION=2.6.11
|
||||||
- MONGODB_VERSION=3.0.8
|
- MONGODB_VERSION=3.0.8
|
||||||
|
|||||||
14
README.md
14
README.md
@@ -135,6 +135,20 @@ PARSE_SERVER_MAX_UPLOAD_SIZE
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
##### Configuring S3 Adapter
|
||||||
|
|
||||||
|
You can use the following environment variable setup the S3 adapter
|
||||||
|
|
||||||
|
```js
|
||||||
|
S3_ACCESS_KEY
|
||||||
|
S3_SECRET_KEY
|
||||||
|
S3_BUCKET
|
||||||
|
S3_REGION
|
||||||
|
S3_BUCKET_PREFIX
|
||||||
|
S3_DIRECT_ACCESS
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
We really want Parse to be yours, to see it grow and thrive in the open source community. Please see the [Contributing to Parse Server guide](CONTRIBUTING.md).
|
We really want Parse to be yours, to see it grow and thrive in the open source community. Please see the [Contributing to Parse Server guide](CONTRIBUTING.md).
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
require("../lib/cli/parse-server");
|
require("../lib/cli/parse-server");
|
||||||
|
|||||||
11
package.json
11
package.json
@@ -10,6 +10,8 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"bin/",
|
"bin/",
|
||||||
"lib/",
|
"lib/",
|
||||||
|
"public_html/",
|
||||||
|
"views/",
|
||||||
"LICENSE",
|
"LICENSE",
|
||||||
"PATENTS",
|
"PATENTS",
|
||||||
"README.md"
|
"README.md"
|
||||||
@@ -54,12 +56,13 @@
|
|||||||
"nodemon": "^1.8.1"
|
"nodemon": "^1.8.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "npm run build && bin/dev",
|
"dev": "npm run build && node bin/dev",
|
||||||
"build": "./node_modules/.bin/babel src/ -d lib/",
|
"build": "./node_modules/.bin/babel src/ -d lib/",
|
||||||
"pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.0.8} ./node_modules/.bin/mongodb-runner start",
|
"pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.0.8} ./node_modules/.bin/mongodb-runner start",
|
||||||
"test": "cross-env NODE_ENV=test TESTING=1 ./node_modules/.bin/babel-node $(if [ \"$CODE_COVERAGE\" = \"1\" ]; then echo './node_modules/babel-istanbul/lib/cli.js cover -x **/spec/**'; fi;) ./node_modules/jasmine/bin/jasmine.js",
|
"test": "cross-env NODE_ENV=test TESTING=1 ./node_modules/.bin/babel-node $COVERAGE_OPTION ./node_modules/jasmine/bin/jasmine.js",
|
||||||
"posttest": "mongodb-runner stop",
|
"posttest": "./node_modules/.bin/mongodb-runner stop",
|
||||||
"start": "./bin/parse-server",
|
"coverage": "cross-env COVERAGE_OPTION='./node_modules/babel-istanbul/lib/cli.js cover -x **/spec/**' npm test",
|
||||||
|
"start": "node ./bin/parse-server",
|
||||||
"prepublish": "npm run build"
|
"prepublish": "npm run build"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
|
||||||
var loadAdapter = require("../src/Adapters/AdapterLoader").loadAdapter;
|
var loadAdapter = require("../src/Adapters/AdapterLoader").loadAdapter;
|
||||||
var FilesAdapter = require("../src/Adapters/Files/FilesAdapter").default;
|
var FilesAdapter = require("../src/Adapters/Files/FilesAdapter").default;
|
||||||
|
var ParsePushAdapter = require("../src/Adapters/Push/ParsePushAdapter");
|
||||||
|
var S3Adapter = require("../src/Adapters/Files/S3Adapter").default;
|
||||||
|
|
||||||
describe("AdapterLoader", ()=>{
|
describe("AdapterLoader", ()=>{
|
||||||
|
|
||||||
@@ -84,4 +86,27 @@ describe("AdapterLoader", ()=>{
|
|||||||
}).not.toThrow("foo is required for that adapter");
|
}).not.toThrow("foo is required for that adapter");
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should load push adapter from options", (done) => {
|
||||||
|
var options = {
|
||||||
|
ios: {
|
||||||
|
bundleId: 'bundle.id'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(() => {
|
||||||
|
var adapter = loadAdapter(undefined, ParsePushAdapter, options);
|
||||||
|
expect(adapter.constructor).toBe(ParsePushAdapter);
|
||||||
|
expect(adapter).not.toBe(undefined);
|
||||||
|
}).not.toThrow();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should load S3Adapter from direct passing", (done) => {
|
||||||
|
var s3Adapter = new S3Adapter("key", "secret", "bucket")
|
||||||
|
expect(() => {
|
||||||
|
var adapter = loadAdapter(s3Adapter, FilesAdapter);
|
||||||
|
expect(adapter).toBe(s3Adapter);
|
||||||
|
}).not.toThrow();
|
||||||
|
done();
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -967,6 +967,23 @@ describe('miscellaneous', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('beforeSave change propagates through the save response', (done) => {
|
||||||
|
Parse.Cloud.beforeSave('ChangingObject', function(request, response) {
|
||||||
|
request.object.set('foo', 'baz');
|
||||||
|
response.success();
|
||||||
|
});
|
||||||
|
let obj = new Parse.Object('ChangingObject');
|
||||||
|
obj.save({ foo: 'bar' }).then((objAgain) => {
|
||||||
|
expect(objAgain.get('foo')).toEqual('baz');
|
||||||
|
Parse.Cloud._removeHook("Triggers", "beforeSave", "ChangingObject");
|
||||||
|
done();
|
||||||
|
}, (e) => {
|
||||||
|
Parse.Cloud._removeHook("Triggers", "beforeSave", "ChangingObject");
|
||||||
|
fail('Should not have failed to save.');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('dedupes an installation properly and returns updatedAt', (done) => {
|
it('dedupes an installation properly and returns updatedAt', (done) => {
|
||||||
let headers = {
|
let headers = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -995,4 +1012,32 @@ describe('miscellaneous', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('android login providing empty authData block works', (done) => {
|
||||||
|
let headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Parse-Application-Id': 'test',
|
||||||
|
'X-Parse-REST-API-Key': 'rest'
|
||||||
|
};
|
||||||
|
let data = {
|
||||||
|
username: 'pulse1989',
|
||||||
|
password: 'password1234',
|
||||||
|
authData: {}
|
||||||
|
};
|
||||||
|
let requestOptions = {
|
||||||
|
headers: headers,
|
||||||
|
url: 'http://localhost:8378/1/users',
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
};
|
||||||
|
request.post(requestOptions, (error, response, body) => {
|
||||||
|
expect(error).toBe(null);
|
||||||
|
requestOptions.url = 'http://localhost:8378/1/login';
|
||||||
|
request.get(requestOptions, (error, response, body) => {
|
||||||
|
expect(error).toBe(null);
|
||||||
|
let b = JSON.parse(body);
|
||||||
|
expect(typeof b['sessionToken']).toEqual('string');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
// Roles are not accessible without the master key, so they are not intended
|
// Roles are not accessible without the master key, so they are not intended
|
||||||
// for use by clients. We can manually test them using the master key.
|
// for use by clients. We can manually test them using the master key.
|
||||||
@@ -64,26 +64,30 @@ describe('Parse Role testing', () => {
|
|||||||
|
|
||||||
var rolesNames = ["FooRole", "BarRole", "BazRole"];
|
var rolesNames = ["FooRole", "BarRole", "BazRole"];
|
||||||
|
|
||||||
var createRole = function(name, parent, user) {
|
var createRole = function(name, sibling, user) {
|
||||||
var role = new Parse.Role(name, new Parse.ACL());
|
var role = new Parse.Role(name, new Parse.ACL());
|
||||||
if (user) {
|
if (user) {
|
||||||
var users = role.relation('users');
|
var users = role.relation('users');
|
||||||
users.add(user);
|
users.add(user);
|
||||||
}
|
}
|
||||||
if (parent) {
|
if (sibling) {
|
||||||
role.relation('roles').add(parent);
|
role.relation('roles').add(sibling);
|
||||||
}
|
}
|
||||||
return role.save({}, { useMasterKey: true });
|
return role.save({}, { useMasterKey: true });
|
||||||
}
|
}
|
||||||
var roleIds = {};
|
var roleIds = {};
|
||||||
createTestUser().then( (user) => {
|
createTestUser().then( (user) => {
|
||||||
|
// Put the user on the 1st role
|
||||||
return createRole(rolesNames[0], null, null).then( (aRole) => {
|
return createRole(rolesNames[0], null, user).then( (aRole) => {
|
||||||
roleIds[aRole.get("name")] = aRole.id;
|
roleIds[aRole.get("name")] = aRole.id;
|
||||||
|
// set the 1st role as a sibling of the second
|
||||||
|
// user will should have 2 role now
|
||||||
return createRole(rolesNames[1], aRole, null);
|
return createRole(rolesNames[1], aRole, null);
|
||||||
}).then( (anotherRole) => {
|
}).then( (anotherRole) => {
|
||||||
roleIds[anotherRole.get("name")] = anotherRole.id;
|
roleIds[anotherRole.get("name")] = anotherRole.id;
|
||||||
return createRole(rolesNames[2], anotherRole, user);
|
// set this role as a sibling of the last
|
||||||
|
// the user should now have 3 roles
|
||||||
|
return createRole(rolesNames[2], anotherRole, null);
|
||||||
}).then( (lastRole) => {
|
}).then( (lastRole) => {
|
||||||
roleIds[lastRole.get("name")] = lastRole.id;
|
roleIds[lastRole.get("name")] = lastRole.id;
|
||||||
var auth = new Auth({ config: new Config("test"), isMaster: true, user: user });
|
var auth = new Auth({ config: new Config("test"), isMaster: true, user: user });
|
||||||
@@ -118,6 +122,80 @@ describe('Parse Role testing', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Should properly resolve roles", (done) => {
|
||||||
|
let admin = new Parse.Role("Admin", new Parse.ACL());
|
||||||
|
let moderator = new Parse.Role("Moderator", new Parse.ACL());
|
||||||
|
let superModerator = new Parse.Role("SuperModerator", new Parse.ACL());
|
||||||
|
let contentManager = new Parse.Role('ContentManager', new Parse.ACL());
|
||||||
|
let superContentManager = new Parse.Role('SuperContentManager', new Parse.ACL());
|
||||||
|
Parse.Object.saveAll([admin, moderator, contentManager, superModerator, superContentManager], {useMasterKey: true}).then(() => {
|
||||||
|
contentManager.getRoles().add([moderator, superContentManager]);
|
||||||
|
moderator.getRoles().add([admin, superModerator]);
|
||||||
|
superContentManager.getRoles().add(superModerator);
|
||||||
|
return Parse.Object.saveAll([admin, moderator, contentManager, superModerator, superContentManager], {useMasterKey: true});
|
||||||
|
}).then(() => {
|
||||||
|
var auth = new Auth({ config: new Config("test"), isMaster: true });
|
||||||
|
// For each role, fetch their sibling, what they inherit
|
||||||
|
// return with result and roleId for later comparison
|
||||||
|
let promises = [admin, moderator, contentManager, superModerator].map((role) => {
|
||||||
|
return auth._getAllRoleNamesForId(role.id).then((result) => {
|
||||||
|
return Parse.Promise.as({
|
||||||
|
id: role.id,
|
||||||
|
name: role.get('name'),
|
||||||
|
roleIds: result
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
return Parse.Promise.when(promises);
|
||||||
|
}).then((results) => {
|
||||||
|
results.forEach((result) => {
|
||||||
|
let id = result.id;
|
||||||
|
let roleIds = result.roleIds;
|
||||||
|
if (id == admin.id) {
|
||||||
|
expect(roleIds.length).toBe(2);
|
||||||
|
expect(roleIds.indexOf(moderator.id)).not.toBe(-1);
|
||||||
|
expect(roleIds.indexOf(contentManager.id)).not.toBe(-1);
|
||||||
|
} else if (id == moderator.id) {
|
||||||
|
expect(roleIds.length).toBe(1);
|
||||||
|
expect(roleIds.indexOf(contentManager.id)).toBe(0);
|
||||||
|
} else if (id == contentManager.id) {
|
||||||
|
expect(roleIds.length).toBe(0);
|
||||||
|
} else if (id == superModerator.id) {
|
||||||
|
expect(roleIds.length).toBe(3);
|
||||||
|
expect(roleIds.indexOf(moderator.id)).not.toBe(-1);
|
||||||
|
expect(roleIds.indexOf(contentManager.id)).not.toBe(-1);
|
||||||
|
expect(roleIds.indexOf(superContentManager.id)).not.toBe(-1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
}).fail((err) => {
|
||||||
|
console.error(err);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can create role and query empty users', (done)=> {
|
||||||
|
var roleACL = new Parse.ACL();
|
||||||
|
roleACL.setPublicReadAccess(true);
|
||||||
|
var role = new Parse.Role('subscribers', roleACL);
|
||||||
|
role.save({}, {useMasterKey : true})
|
||||||
|
.then((x)=>{
|
||||||
|
var query = role.relation('users').query();
|
||||||
|
query.find({useMasterKey : true})
|
||||||
|
.then((users)=>{
|
||||||
|
done();
|
||||||
|
}, (e)=>{
|
||||||
|
fail('should not have errors');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
}, (e) => {
|
||||||
|
console.log(e);
|
||||||
|
fail('should not have errored');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -175,17 +175,26 @@ describe('rest create', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
var newUserSignedUpByFacebookObjectId;
|
||||||
rest.create(config, auth.nobody(config), '_User', data)
|
rest.create(config, auth.nobody(config), '_User', data)
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
expect(typeof r.response.objectId).toEqual('string');
|
expect(typeof r.response.objectId).toEqual('string');
|
||||||
expect(typeof r.response.createdAt).toEqual('string');
|
expect(typeof r.response.createdAt).toEqual('string');
|
||||||
expect(typeof r.response.sessionToken).toEqual('string');
|
expect(typeof r.response.sessionToken).toEqual('string');
|
||||||
|
newUserSignedUpByFacebookObjectId = r.response.objectId;
|
||||||
return rest.create(config, auth.nobody(config), '_User', data);
|
return rest.create(config, auth.nobody(config), '_User', data);
|
||||||
}).then((r) => {
|
}).then((r) => {
|
||||||
expect(typeof r.response.objectId).toEqual('string');
|
expect(typeof r.response.objectId).toEqual('string');
|
||||||
expect(typeof r.response.createdAt).toEqual('string');
|
expect(typeof r.response.createdAt).toEqual('string');
|
||||||
expect(typeof r.response.username).toEqual('string');
|
expect(typeof r.response.username).toEqual('string');
|
||||||
expect(typeof r.response.updatedAt).toEqual('string');
|
expect(typeof r.response.updatedAt).toEqual('string');
|
||||||
|
expect(r.response.objectId).toEqual(newUserSignedUpByFacebookObjectId);
|
||||||
|
return rest.find(config, auth.master(config),
|
||||||
|
'_Session', {sessionToken: r.response.sessionToken});
|
||||||
|
}).then((response) => {
|
||||||
|
expect(response.results.length).toEqual(1);
|
||||||
|
var output = response.results[0];
|
||||||
|
expect(output.user.objectId).toEqual(newUserSignedUpByFacebookObjectId);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -180,20 +180,18 @@ describe('Schema', () => {
|
|||||||
|
|
||||||
it('will fail to create a class if that class was already created by an object', done => {
|
it('will fail to create a class if that class was already created by an object', done => {
|
||||||
config.database.loadSchema()
|
config.database.loadSchema()
|
||||||
.then(schema => {
|
.then(schema => {
|
||||||
schema.validateObject('NewClass', {foo: 7})
|
schema.validateObject('NewClass', { foo: 7 })
|
||||||
.then(() => {
|
.then(() => schema.reloadData())
|
||||||
schema.reload()
|
.then(() => schema.addClassIfNotExists('NewClass', {
|
||||||
.then(schema => schema.addClassIfNotExists('NewClass', {
|
foo: { type: 'String' }
|
||||||
foo: {type: 'String'}
|
}))
|
||||||
}))
|
.catch(error => {
|
||||||
.catch(error => {
|
expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
|
||||||
expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
|
expect(error.message).toEqual('Class NewClass already exists.');
|
||||||
expect(error.message).toEqual('Class NewClass already exists.');
|
done();
|
||||||
done();
|
});
|
||||||
});
|
|
||||||
});
|
});
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('will resolve class creation races appropriately', done => {
|
it('will resolve class creation races appropriately', done => {
|
||||||
@@ -579,6 +577,38 @@ describe('Schema', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can delete relation field when related _Join collection not exist', done => {
|
||||||
|
config.database.loadSchema()
|
||||||
|
.then(schema => {
|
||||||
|
schema.addClassIfNotExists('NewClass', {
|
||||||
|
relationField: {type: 'Relation', targetClass: '_User'}
|
||||||
|
})
|
||||||
|
.then(mongoObj => {
|
||||||
|
expect(mongoObj).toEqual({
|
||||||
|
_id: 'NewClass',
|
||||||
|
objectId: 'string',
|
||||||
|
updatedAt: 'string',
|
||||||
|
createdAt: 'string',
|
||||||
|
relationField: 'relation<_User>',
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(() => config.database.collectionExists('_Join:relationField:NewClass'))
|
||||||
|
.then(exist => {
|
||||||
|
expect(exist).toEqual(false);
|
||||||
|
})
|
||||||
|
.then(() => schema.deleteField('relationField', 'NewClass', config.database))
|
||||||
|
.then(() => schema.reloadData())
|
||||||
|
.then(() => {
|
||||||
|
expect(schema['data']['NewClass']).toEqual({
|
||||||
|
objectId: 'string',
|
||||||
|
updatedAt: 'string',
|
||||||
|
createdAt: 'string'
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('can delete string fields and resave as number field', done => {
|
it('can delete string fields and resave as number field', done => {
|
||||||
Parse.Object.disableSingleInstance();
|
Parse.Object.disableSingleInstance();
|
||||||
var obj1 = hasAllPODobject();
|
var obj1 = hasAllPODobject();
|
||||||
|
|||||||
@@ -561,6 +561,63 @@ describe('schemas', () => {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('lets you add fields to system schema', done => {
|
||||||
|
request.post({
|
||||||
|
url: 'http://localhost:8378/1/schemas/_User',
|
||||||
|
headers: masterKeyHeaders,
|
||||||
|
json: true
|
||||||
|
}, (error, response, body) => {
|
||||||
|
request.put({
|
||||||
|
url: 'http://localhost:8378/1/schemas/_User',
|
||||||
|
headers: masterKeyHeaders,
|
||||||
|
json: true,
|
||||||
|
body: {
|
||||||
|
fields: {
|
||||||
|
newField: {type: 'String'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(body).toEqual({
|
||||||
|
className: '_User',
|
||||||
|
fields: {
|
||||||
|
objectId: {type: 'String'},
|
||||||
|
updatedAt: {type: 'Date'},
|
||||||
|
createdAt: {type: 'Date'},
|
||||||
|
username: {type: 'String'},
|
||||||
|
password: {type: 'String'},
|
||||||
|
authData: {type: 'Object'},
|
||||||
|
email: {type: 'String'},
|
||||||
|
emailVerified: {type: 'Boolean'},
|
||||||
|
newField: {type: 'String'},
|
||||||
|
ACL: {type: 'ACL'}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
request.get({
|
||||||
|
url: 'http://localhost:8378/1/schemas/_User',
|
||||||
|
headers: masterKeyHeaders,
|
||||||
|
json: true
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(body).toEqual({
|
||||||
|
className: '_User',
|
||||||
|
fields: {
|
||||||
|
objectId: {type: 'String'},
|
||||||
|
updatedAt: {type: 'Date'},
|
||||||
|
createdAt: {type: 'Date'},
|
||||||
|
username: {type: 'String'},
|
||||||
|
password: {type: 'String'},
|
||||||
|
authData: {type: 'Object'},
|
||||||
|
email: {type: 'String'},
|
||||||
|
emailVerified: {type: 'Boolean'},
|
||||||
|
newField: {type: 'String'},
|
||||||
|
ACL: {type: 'ACL'}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
it('lets you delete multiple fields and add fields', done => {
|
it('lets you delete multiple fields and add fields', done => {
|
||||||
var obj1 = hasAllPODobject();
|
var obj1 = hasAllPODobject();
|
||||||
obj1.save()
|
obj1.save()
|
||||||
|
|||||||
@@ -28,15 +28,8 @@ export function loadAdapter(adapter, defaultAdapter, options) {
|
|||||||
return loadAdapter(adapter.class, undefined, adapter.options);
|
return loadAdapter(adapter.class, undefined, adapter.options);
|
||||||
} else if (adapter.adapter) {
|
} else if (adapter.adapter) {
|
||||||
return loadAdapter(adapter.adapter, undefined, adapter.options);
|
return loadAdapter(adapter.adapter, undefined, adapter.options);
|
||||||
} else {
|
|
||||||
// Try to load the defaultAdapter with the options
|
|
||||||
// The default adapter should throw if the options are
|
|
||||||
// incompatible
|
|
||||||
try {
|
|
||||||
return loadAdapter(defaultAdapter, undefined, adapter);
|
|
||||||
} catch (e) {};
|
|
||||||
}
|
}
|
||||||
// return the adapter as is as it's unusable otherwise
|
// return the adapter as provided
|
||||||
return adapter;
|
return adapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,19 +8,20 @@ import requiredParameter from '../../requiredParameter';
|
|||||||
|
|
||||||
const DEFAULT_S3_REGION = "us-east-1";
|
const DEFAULT_S3_REGION = "us-east-1";
|
||||||
|
|
||||||
function parseS3AdapterOptions(...options) {
|
function requiredOrFromEnvironment(env, name) {
|
||||||
if (options.length === 1 && typeof options[0] == "object") {
|
let environmentVariable = process.env[env];
|
||||||
return options;
|
if (!environmentVariable) {
|
||||||
|
requiredParameter(`S3Adapter requires an ${name}`);
|
||||||
}
|
}
|
||||||
|
return environmentVariable;
|
||||||
const additionalOptions = options[3] || {};
|
}
|
||||||
|
|
||||||
return {
|
function fromEnvironmentOrDefault(env, defaultValue) {
|
||||||
accessKey: options[0],
|
let environmentVariable = process.env[env];
|
||||||
secretKey: options[1],
|
if (environmentVariable) {
|
||||||
bucket: options[2],
|
return environmentVariable;
|
||||||
region: additionalOptions.region
|
|
||||||
}
|
}
|
||||||
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class S3Adapter extends FilesAdapter {
|
export class S3Adapter extends FilesAdapter {
|
||||||
@@ -28,12 +29,12 @@ export class S3Adapter extends FilesAdapter {
|
|||||||
// Providing AWS access and secret keys is mandatory
|
// Providing AWS access and secret keys is mandatory
|
||||||
// Region and bucket will use sane defaults if omitted
|
// Region and bucket will use sane defaults if omitted
|
||||||
constructor(
|
constructor(
|
||||||
accessKey = requiredParameter('S3Adapter requires an accessKey'),
|
accessKey = requiredOrFromEnvironment('S3_ACCESS_KEY', 'accessKey'),
|
||||||
secretKey = requiredParameter('S3Adapter requires a secretKey'),
|
secretKey = requiredOrFromEnvironment('S3_SECRET_KEY', 'secretKey'),
|
||||||
bucket,
|
bucket = fromEnvironmentOrDefault('S3_BUCKET', undefined),
|
||||||
{ region = DEFAULT_S3_REGION,
|
{ region = fromEnvironmentOrDefault('S3_REGION', DEFAULT_S3_REGION),
|
||||||
bucketPrefix = '',
|
bucketPrefix = fromEnvironmentOrDefault('S3_BUCKET_PREFIX', ''),
|
||||||
directAccess = false } = {}) {
|
directAccess = fromEnvironmentOrDefault('S3_DIRECT_ACCESS', false) } = {}) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this._region = region;
|
this._region = region;
|
||||||
|
|||||||
23
src/Auth.js
23
src/Auth.js
@@ -139,18 +139,18 @@ Auth.prototype._loadRoles = function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Given a role object id, get any other roles it is part of
|
// Given a role object id, get any other roles it is part of
|
||||||
// TODO: Make recursive to support role nesting beyond 1 level deep
|
|
||||||
Auth.prototype._getAllRoleNamesForId = function(roleID) {
|
Auth.prototype._getAllRoleNamesForId = function(roleID) {
|
||||||
|
|
||||||
|
// As per documentation, a Role inherits AnotherRole
|
||||||
|
// if this Role is in the roles pointer of this AnotherRole
|
||||||
|
// Let's find all the roles where this role is in a roles relation
|
||||||
var rolePointer = {
|
var rolePointer = {
|
||||||
__type: 'Pointer',
|
__type: 'Pointer',
|
||||||
className: '_Role',
|
className: '_Role',
|
||||||
objectId: roleID
|
objectId: roleID
|
||||||
};
|
};
|
||||||
var restWhere = {
|
var restWhere = {
|
||||||
'$relatedTo': {
|
'roles': rolePointer
|
||||||
key: 'roles',
|
|
||||||
object: rolePointer
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
var query = new RestQuery(this.config, master(this.config), '_Role',
|
var query = new RestQuery(this.config, master(this.config), '_Role',
|
||||||
restWhere, {});
|
restWhere, {});
|
||||||
@@ -161,6 +161,10 @@ Auth.prototype._getAllRoleNamesForId = function(roleID) {
|
|||||||
}
|
}
|
||||||
var roleIDs = results.map(r => r.objectId);
|
var roleIDs = results.map(r => r.objectId);
|
||||||
|
|
||||||
|
// we found a list of roles where the roleID
|
||||||
|
// is referenced in the roles relation,
|
||||||
|
// Get the roles where those found roles are also
|
||||||
|
// referenced the same way
|
||||||
var parentRolesPromises = roleIDs.map( (roleId) => {
|
var parentRolesPromises = roleIDs.map( (roleId) => {
|
||||||
return this._getAllRoleNamesForId(roleId);
|
return this._getAllRoleNamesForId(roleId);
|
||||||
});
|
});
|
||||||
@@ -169,14 +173,9 @@ Auth.prototype._getAllRoleNamesForId = function(roleID) {
|
|||||||
}).then(function(results){
|
}).then(function(results){
|
||||||
// Flatten
|
// Flatten
|
||||||
let roleIDs = results.reduce( (memo, result) => {
|
let roleIDs = results.reduce( (memo, result) => {
|
||||||
if (typeof result == "object") {
|
return memo.concat(result);
|
||||||
memo = memo.concat(result);
|
|
||||||
} else {
|
|
||||||
memo.push(result);
|
|
||||||
}
|
|
||||||
return memo;
|
|
||||||
}, []);
|
}, []);
|
||||||
return Promise.resolve(roleIDs);
|
return Promise.resolve([...new Set(roleIDs)]);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ DatabaseController.prototype.loadSchema = function(acceptor = returnsTrue) {
|
|||||||
DatabaseController.prototype.redirectClassNameForKey = function(className, key) {
|
DatabaseController.prototype.redirectClassNameForKey = function(className, key) {
|
||||||
return this.loadSchema().then((schema) => {
|
return this.loadSchema().then((schema) => {
|
||||||
var t = schema.getExpectedType(className, key);
|
var t = schema.getExpectedType(className, key);
|
||||||
var match = t.match(/^relation<(.*)>$/);
|
var match = t ? t.match(/^relation<(.*)>$/) : false;
|
||||||
if (match) {
|
if (match) {
|
||||||
return match[1];
|
return match[1];
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -396,6 +396,7 @@ RestQuery.prototype.runCount = function() {
|
|||||||
}
|
}
|
||||||
this.findOptions.count = true;
|
this.findOptions.count = true;
|
||||||
delete this.findOptions.skip;
|
delete this.findOptions.skip;
|
||||||
|
delete this.findOptions.limit;
|
||||||
return this.config.database.find(
|
return this.config.database.find(
|
||||||
this.className, this.restWhere, this.findOptions).then((c) => {
|
this.className, this.restWhere, this.findOptions).then((c) => {
|
||||||
this.response.count = c;
|
this.response.count = c;
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ RestWrite.prototype.runBeforeTrigger = function() {
|
|||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
if (response && response.object) {
|
if (response && response.object) {
|
||||||
this.data = response.object;
|
this.data = response.object;
|
||||||
|
this.storage['changedByTrigger'] = true;
|
||||||
// We should delete the objectId for an update write
|
// We should delete the objectId for an update write
|
||||||
if (this.query && this.query.objectId) {
|
if (this.query && this.query.objectId) {
|
||||||
delete this.data.objectId
|
delete this.data.objectId
|
||||||
@@ -178,7 +179,11 @@ RestWrite.prototype.setRequiredFieldsIfNeeded = function() {
|
|||||||
this.data.updatedAt = this.updatedAt;
|
this.data.updatedAt = this.updatedAt;
|
||||||
if (!this.query) {
|
if (!this.query) {
|
||||||
this.data.createdAt = this.updatedAt;
|
this.data.createdAt = this.updatedAt;
|
||||||
this.data.objectId = cryptoUtils.newObjectId();
|
|
||||||
|
// Only assign new objectId if we are creating new object
|
||||||
|
if (!this.data.objectId) {
|
||||||
|
this.data.objectId = cryptoUtils.newObjectId();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
@@ -802,6 +807,9 @@ RestWrite.prototype.runDatabaseOperation = function() {
|
|||||||
objectId: this.data.objectId,
|
objectId: this.data.objectId,
|
||||||
createdAt: this.data.createdAt
|
createdAt: this.data.createdAt
|
||||||
};
|
};
|
||||||
|
if (this.storage['changedByTrigger']) {
|
||||||
|
Object.assign(resp, this.data);
|
||||||
|
}
|
||||||
if (this.storage['token']) {
|
if (this.storage['token']) {
|
||||||
resp.sessionToken = this.storage['token'];
|
resp.sessionToken = this.storage['token'];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ function modifySchema(req) {
|
|||||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${req.params.className} does not exist.`);
|
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${req.params.className} does not exist.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
let existingFields = schema.data[className];
|
let existingFields = Object.assign(schema.data[className], {_id: className});
|
||||||
Object.keys(submittedFields).forEach(name => {
|
Object.keys(submittedFields).forEach(name => {
|
||||||
let field = submittedFields[name];
|
let field = submittedFields[name];
|
||||||
if (existingFields[name] && field.__op !== 'Delete') {
|
if (existingFields[name] && field.__op !== 'Delete') {
|
||||||
|
|||||||
785
src/Schema.js
785
src/Schema.js
@@ -71,7 +71,6 @@ var defaultColumns = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
var requiredColumns = {
|
var requiredColumns = {
|
||||||
_Product: ["productIdentifier", "icon", "order", "title", "subtitle"],
|
_Product: ["productIdentifier", "icon", "order", "title", "subtitle"],
|
||||||
_Role: ["name", "ACL"]
|
_Role: ["name", "ACL"]
|
||||||
@@ -168,54 +167,380 @@ function schemaAPITypeToMongoFieldType(type) {
|
|||||||
// '_id' indicates the className
|
// '_id' indicates the className
|
||||||
// '_metadata' is ignored for now
|
// '_metadata' is ignored for now
|
||||||
// Everything else is expected to be a userspace field.
|
// Everything else is expected to be a userspace field.
|
||||||
function Schema(collection, mongoSchema) {
|
class Schema {
|
||||||
this.collection = collection;
|
collection;
|
||||||
|
data;
|
||||||
|
perms;
|
||||||
|
|
||||||
// this.data[className][fieldName] tells you the type of that field
|
constructor(collection) {
|
||||||
this.data = {};
|
this.collection = collection;
|
||||||
// this.perms[className][operation] tells you the acl-style permissions
|
|
||||||
this.perms = {};
|
|
||||||
|
|
||||||
for (var obj of mongoSchema) {
|
// this.data[className][fieldName] tells you the type of that field
|
||||||
var className = null;
|
this.data = {};
|
||||||
var classData = {};
|
// this.perms[className][operation] tells you the acl-style permissions
|
||||||
var permsData = null;
|
this.perms = {};
|
||||||
for (var key in obj) {
|
|
||||||
var value = obj[key];
|
|
||||||
switch(key) {
|
|
||||||
case '_id':
|
|
||||||
className = value;
|
|
||||||
break;
|
|
||||||
case '_metadata':
|
|
||||||
if (value && value['class_permissions']) {
|
|
||||||
permsData = value['class_permissions'];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
classData[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (className) {
|
|
||||||
this.data[className] = classData;
|
|
||||||
if (permsData) {
|
|
||||||
this.perms[className] = permsData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reloadData() {
|
||||||
|
this.data = {};
|
||||||
|
this.perms = {};
|
||||||
|
return this.collection.find({}, {}).toArray().then(mongoSchema => {
|
||||||
|
for (let obj of mongoSchema) {
|
||||||
|
let className = null;
|
||||||
|
let classData = {};
|
||||||
|
let permsData = null;
|
||||||
|
Object.keys(obj).forEach(key => {
|
||||||
|
let value = obj[key];
|
||||||
|
switch (key) {
|
||||||
|
case '_id':
|
||||||
|
className = value;
|
||||||
|
break;
|
||||||
|
case '_metadata':
|
||||||
|
if (value && value['class_permissions']) {
|
||||||
|
permsData = value['class_permissions'];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
classData[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (className) {
|
||||||
|
this.data[className] = classData;
|
||||||
|
if (permsData) {
|
||||||
|
this.perms[className] = permsData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new class that includes the three default fields.
|
||||||
|
// ACL is an implicit column that does not get an entry in the
|
||||||
|
// _SCHEMAS database. Returns a promise that resolves with the
|
||||||
|
// created schema, in mongo format.
|
||||||
|
// on success, and rejects with an error on fail. Ensure you
|
||||||
|
// have authorization (master key, or client class creation
|
||||||
|
// enabled) before calling this function.
|
||||||
|
addClassIfNotExists(className, fields) {
|
||||||
|
if (this.data[className]) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mongoObject = mongoSchemaFromFieldsAndClassName(fields, className);
|
||||||
|
if (!mongoObject.result) {
|
||||||
|
return Promise.reject(mongoObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.collection.insertOne(mongoObject.result)
|
||||||
|
.then(result => result.ops[0])
|
||||||
|
.catch(error => {
|
||||||
|
if (error.code === 11000) { //Mongo's duplicate key error
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`);
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Returns whether the schema knows the type of all these keys.
|
||||||
|
hasKeys(className, keys) {
|
||||||
|
for (var key of keys) {
|
||||||
|
if (!this.data[className] || !this.data[className][key]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise that resolves successfully to the new schema
|
||||||
|
// object or fails with a reason.
|
||||||
|
// If 'freeze' is true, refuse to update the schema.
|
||||||
|
// WARNING: this function has side-effects, and doesn't actually
|
||||||
|
// do any validation of the format of the className. You probably
|
||||||
|
// should use classNameIsValid or addClassIfNotExists or something
|
||||||
|
// like that instead. TODO: rename or remove this function.
|
||||||
|
validateClassName(className, freeze) {
|
||||||
|
if (this.data[className]) {
|
||||||
|
return Promise.resolve(this);
|
||||||
|
}
|
||||||
|
if (freeze) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_JSON,
|
||||||
|
'schema is frozen, cannot add: ' + className);
|
||||||
|
}
|
||||||
|
// We don't have this class. Update the schema
|
||||||
|
return this.collection.insert([{_id: className}]).then(() => {
|
||||||
|
// The schema update succeeded. Reload the schema
|
||||||
|
return this.reloadData();
|
||||||
|
}, () => {
|
||||||
|
// The schema update failed. This can be okay - it might
|
||||||
|
// have failed because there's a race condition and a different
|
||||||
|
// client is making the exact same schema update that we want.
|
||||||
|
// So just reload the schema.
|
||||||
|
return this.reloadData();
|
||||||
|
}).then(() => {
|
||||||
|
// Ensure that the schema now validates
|
||||||
|
return this.validateClassName(className, true);
|
||||||
|
}, (error) => {
|
||||||
|
// The schema still doesn't validate. Give up
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_JSON,
|
||||||
|
'schema class name does not revalidate');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets the Class-level permissions for a given className, which must exist.
|
||||||
|
setPermissions(className, perms) {
|
||||||
|
var query = {_id: className};
|
||||||
|
var update = {
|
||||||
|
_metadata: {
|
||||||
|
class_permissions: perms
|
||||||
|
}
|
||||||
|
};
|
||||||
|
update = {'$set': update};
|
||||||
|
return this.collection.findAndModify(query, {}, update, {}).then(() => {
|
||||||
|
// The update succeeded. Reload the schema
|
||||||
|
return this.reloadData();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise that resolves successfully to the new schema
|
||||||
|
// object if the provided className-key-type tuple is valid.
|
||||||
|
// The className must already be validated.
|
||||||
|
// If 'freeze' is true, refuse to update the schema for this field.
|
||||||
|
validateField(className, key, type, freeze) {
|
||||||
|
// Just to check that the key is valid
|
||||||
|
transform.transformKey(this, className, key);
|
||||||
|
|
||||||
|
if( key.indexOf(".") > 0 ) {
|
||||||
|
// subdocument key (x.y) => ok if x is of type 'object'
|
||||||
|
key = key.split(".")[ 0 ];
|
||||||
|
type = 'object';
|
||||||
|
}
|
||||||
|
|
||||||
|
var expected = this.data[className][key];
|
||||||
|
if (expected) {
|
||||||
|
expected = (expected === 'map' ? 'object' : expected);
|
||||||
|
if (expected === type) {
|
||||||
|
return Promise.resolve(this);
|
||||||
|
} else {
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.INCORRECT_TYPE,
|
||||||
|
'schema mismatch for ' + className + '.' + key +
|
||||||
|
'; expected ' + expected + ' but got ' + type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (freeze) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_JSON,
|
||||||
|
'schema is frozen, cannot add ' + key + ' field');
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't have this field, but if the value is null or undefined,
|
||||||
|
// we won't update the schema until we get a value with a type.
|
||||||
|
if (!type) {
|
||||||
|
return Promise.resolve(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'geopoint') {
|
||||||
|
// Make sure there are not other geopoint fields
|
||||||
|
for (var otherKey in this.data[className]) {
|
||||||
|
if (this.data[className][otherKey] === 'geopoint') {
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.INCORRECT_TYPE,
|
||||||
|
'there can only be one geopoint field in a class');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't have this field. Update the schema.
|
||||||
|
// Note that we use the $exists guard and $set to avoid race
|
||||||
|
// conditions in the database. This is important!
|
||||||
|
var query = {_id: className};
|
||||||
|
query[key] = {'$exists': false};
|
||||||
|
var update = {};
|
||||||
|
update[key] = type;
|
||||||
|
update = {'$set': update};
|
||||||
|
return this.collection.findAndModify(query, {}, update, {}).then(() => {
|
||||||
|
// The update succeeded. Reload the schema
|
||||||
|
return this.reloadData();
|
||||||
|
}, () => {
|
||||||
|
// The update failed. This can be okay - it might have been a race
|
||||||
|
// condition where another client updated the schema in the same
|
||||||
|
// way that we wanted to. So, just reload the schema
|
||||||
|
return this.reloadData();
|
||||||
|
}).then(() => {
|
||||||
|
// Ensure that the schema now validates
|
||||||
|
return this.validateField(className, key, type, true);
|
||||||
|
}, (error) => {
|
||||||
|
// The schema still doesn't validate. Give up
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_JSON,
|
||||||
|
'schema key will not revalidate');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a field, and remove that data from all objects. This is intended
|
||||||
|
// to remove unused fields, if other writers are writing objects that include
|
||||||
|
// this field, the field may reappear. Returns a Promise that resolves with
|
||||||
|
// no object on success, or rejects with { code, error } on failure.
|
||||||
|
// Passing the database and prefix is necessary in order to drop relation collections
|
||||||
|
// and remove fields from objects. Ideally the database would belong to
|
||||||
|
// a database adapter and this function would close over it or access it via member.
|
||||||
|
deleteField(fieldName, className, database) {
|
||||||
|
if (!classNameIsValid(className)) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, invalidClassNameMessage(className));
|
||||||
|
}
|
||||||
|
if (!fieldNameIsValid(fieldName)) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `invalid field name: ${fieldName}`);
|
||||||
|
}
|
||||||
|
//Don't allow deleting the default fields.
|
||||||
|
if (!fieldNameIsValidForClass(fieldName, className)) {
|
||||||
|
throw new Parse.Error(136, `field ${fieldName} cannot be changed`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.reloadData()
|
||||||
|
.then(() => {
|
||||||
|
return this.hasClass(className)
|
||||||
|
.then(hasClass => {
|
||||||
|
if (!hasClass) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} does not exist.`);
|
||||||
|
}
|
||||||
|
if (!this.data[className][fieldName]) {
|
||||||
|
throw new Parse.Error(255, `Field ${fieldName} does not exist, cannot delete.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.data[className][fieldName].startsWith('relation<')) {
|
||||||
|
//For relations, drop the _Join table
|
||||||
|
return database.collectionExists(`_Join:${fieldName}:${className}`).then(exist => {
|
||||||
|
if (exist) {
|
||||||
|
return database.dropCollection(`_Join:${fieldName}:${className}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// for non-relations, remove all the data.
|
||||||
|
// This is necessary to ensure that the data is still gone if they add the same field.
|
||||||
|
return database.collection(className)
|
||||||
|
.then(collection => {
|
||||||
|
var mongoFieldName = this.data[className][fieldName].startsWith('*') ? '_p_' + fieldName : fieldName;
|
||||||
|
return collection.update({}, { "$unset": { [mongoFieldName] : null } }, { multi: true });
|
||||||
|
});
|
||||||
|
})
|
||||||
|
// Save the _SCHEMA object
|
||||||
|
.then(() => this.collection.update({ _id: className }, { $unset: {[fieldName]: null }}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validates an object provided in REST format.
|
||||||
|
// Returns a promise that resolves to the new schema if this object is
|
||||||
|
// valid.
|
||||||
|
validateObject(className, object, query) {
|
||||||
|
var geocount = 0;
|
||||||
|
var promise = this.validateClassName(className);
|
||||||
|
for (var key in object) {
|
||||||
|
if (object[key] === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var expected = getType(object[key]);
|
||||||
|
if (expected === 'geopoint') {
|
||||||
|
geocount++;
|
||||||
|
}
|
||||||
|
if (geocount > 1) {
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.INCORRECT_TYPE,
|
||||||
|
'there can only be one geopoint field in a class');
|
||||||
|
}
|
||||||
|
if (!expected) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
promise = thenValidateField(promise, className, key, expected);
|
||||||
|
}
|
||||||
|
promise = thenValidateRequiredColumns(promise, className, object, query);
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validates that all the properties are set for the object
|
||||||
|
validateRequiredColumns(className, object, query) {
|
||||||
|
var columns = requiredColumns[className];
|
||||||
|
if (!columns || columns.length == 0) {
|
||||||
|
return Promise.resolve(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
var missingColumns = columns.filter(function(column){
|
||||||
|
if (query && query.objectId) {
|
||||||
|
if (object[column] && typeof object[column] === "object") {
|
||||||
|
// Trying to delete a required column
|
||||||
|
return object[column].__op == 'Delete';
|
||||||
|
}
|
||||||
|
// Not trying to do anything there
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !object[column]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (missingColumns.length > 0) {
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.INCORRECT_TYPE,
|
||||||
|
missingColumns[0]+' is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validates an operation passes class-level-permissions set in the schema
|
||||||
|
validatePermission(className, aclGroup, operation) {
|
||||||
|
if (!this.perms[className] || !this.perms[className][operation]) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
var perms = this.perms[className][operation];
|
||||||
|
// Handle the public scenario quickly
|
||||||
|
if (perms['*']) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
// Check permissions against the aclGroup provided (array of userId/roles)
|
||||||
|
var found = false;
|
||||||
|
for (var i = 0; i < aclGroup.length && !found; i++) {
|
||||||
|
if (perms[aclGroup[i]]) {
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
// TODO: Verify correct error code
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
'Permission denied for this action.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Returns the expected type for a className+key combination
|
||||||
|
// or undefined if the schema is not set
|
||||||
|
getExpectedType(className, key) {
|
||||||
|
if (this.data && this.data[className]) {
|
||||||
|
return this.data[className][key];
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Checks if a given class is in the schema. Needs to load the
|
||||||
|
// schema first, which is kinda janky. Hopefully we can refactor
|
||||||
|
// and make this be a regular value.
|
||||||
|
hasClass(className) {
|
||||||
|
return this.reloadData().then(() => !!(this.data[className]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to check if a field is a pointer, returns true or false.
|
||||||
|
isPointer(className, key) {
|
||||||
|
var expected = this.getExpectedType(className, key);
|
||||||
|
if (expected && expected.charAt(0) == '*') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a promise for a new Schema.
|
// Returns a promise for a new Schema.
|
||||||
function load(collection) {
|
function load(collection) {
|
||||||
return collection.find({}, {}).toArray().then((mongoSchema) => {
|
let schema = new Schema(collection);
|
||||||
return new Schema(collection, mongoSchema);
|
return schema.reloadData().then(() => schema);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a new, reloaded schema.
|
|
||||||
Schema.prototype.reload = function() {
|
|
||||||
return load(this.collection);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Returns { code, error } if invalid, or { result }, an object
|
// Returns { code, error } if invalid, or { result }, an object
|
||||||
// suitable for inserting into _SCHEMA collection, otherwise
|
// suitable for inserting into _SCHEMA collection, otherwise
|
||||||
function mongoSchemaFromFieldsAndClassName(fields, className) {
|
function mongoSchemaFromFieldsAndClassName(fields, className) {
|
||||||
@@ -331,218 +656,6 @@ function buildMergedSchemaObject(mongoObject, putRequest) {
|
|||||||
return newSchema;
|
return newSchema;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new class that includes the three default fields.
|
|
||||||
// ACL is an implicit column that does not get an entry in the
|
|
||||||
// _SCHEMAS database. Returns a promise that resolves with the
|
|
||||||
// created schema, in mongo format.
|
|
||||||
// on success, and rejects with an error on fail. Ensure you
|
|
||||||
// have authorization (master key, or client class creation
|
|
||||||
// enabled) before calling this function.
|
|
||||||
Schema.prototype.addClassIfNotExists = function(className, fields) {
|
|
||||||
if (this.data[className]) {
|
|
||||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mongoObject = mongoSchemaFromFieldsAndClassName(fields, className);
|
|
||||||
if (!mongoObject.result) {
|
|
||||||
return Promise.reject(mongoObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.collection.insertOne(mongoObject.result)
|
|
||||||
.then(result => result.ops[0])
|
|
||||||
.catch(error => {
|
|
||||||
if (error.code === 11000) { //Mongo's duplicate key error
|
|
||||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`);
|
|
||||||
}
|
|
||||||
return Promise.reject(error);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Returns a promise that resolves successfully to the new schema
|
|
||||||
// object or fails with a reason.
|
|
||||||
// If 'freeze' is true, refuse to update the schema.
|
|
||||||
// WARNING: this function has side-effects, and doesn't actually
|
|
||||||
// do any validation of the format of the className. You probably
|
|
||||||
// should use classNameIsValid or addClassIfNotExists or something
|
|
||||||
// like that instead. TODO: rename or remove this function.
|
|
||||||
Schema.prototype.validateClassName = function(className, freeze) {
|
|
||||||
if (this.data[className]) {
|
|
||||||
return Promise.resolve(this);
|
|
||||||
}
|
|
||||||
if (freeze) {
|
|
||||||
throw new Parse.Error(Parse.Error.INVALID_JSON,
|
|
||||||
'schema is frozen, cannot add: ' + className);
|
|
||||||
}
|
|
||||||
// We don't have this class. Update the schema
|
|
||||||
return this.collection.insert([{_id: className}]).then(() => {
|
|
||||||
// The schema update succeeded. Reload the schema
|
|
||||||
return this.reload();
|
|
||||||
}, () => {
|
|
||||||
// The schema update failed. This can be okay - it might
|
|
||||||
// have failed because there's a race condition and a different
|
|
||||||
// client is making the exact same schema update that we want.
|
|
||||||
// So just reload the schema.
|
|
||||||
return this.reload();
|
|
||||||
}).then((schema) => {
|
|
||||||
// Ensure that the schema now validates
|
|
||||||
return schema.validateClassName(className, true);
|
|
||||||
}, (error) => {
|
|
||||||
// The schema still doesn't validate. Give up
|
|
||||||
throw new Parse.Error(Parse.Error.INVALID_JSON,
|
|
||||||
'schema class name does not revalidate');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Returns whether the schema knows the type of all these keys.
|
|
||||||
Schema.prototype.hasKeys = function(className, keys) {
|
|
||||||
for (var key of keys) {
|
|
||||||
if (!this.data[className] || !this.data[className][key]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sets the Class-level permissions for a given className, which must
|
|
||||||
// exist.
|
|
||||||
Schema.prototype.setPermissions = function(className, perms) {
|
|
||||||
var query = {_id: className};
|
|
||||||
var update = {
|
|
||||||
_metadata: {
|
|
||||||
class_permissions: perms
|
|
||||||
}
|
|
||||||
};
|
|
||||||
update = {'$set': update};
|
|
||||||
return this.collection.findAndModify(query, {}, update, {}).then(() => {
|
|
||||||
// The update succeeded. Reload the schema
|
|
||||||
return this.reload();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Returns a promise that resolves successfully to the new schema
|
|
||||||
// object if the provided className-key-type tuple is valid.
|
|
||||||
// The className must already be validated.
|
|
||||||
// If 'freeze' is true, refuse to update the schema for this field.
|
|
||||||
Schema.prototype.validateField = function(className, key, type, freeze) {
|
|
||||||
// Just to check that the key is valid
|
|
||||||
transform.transformKey(this, className, key);
|
|
||||||
|
|
||||||
if( key.indexOf(".") > 0 ) {
|
|
||||||
// subdocument key (x.y) => ok if x is of type 'object'
|
|
||||||
key = key.split(".")[ 0 ];
|
|
||||||
type = 'object';
|
|
||||||
}
|
|
||||||
|
|
||||||
var expected = this.data[className][key];
|
|
||||||
if (expected) {
|
|
||||||
expected = (expected === 'map' ? 'object' : expected);
|
|
||||||
if (expected === type) {
|
|
||||||
return Promise.resolve(this);
|
|
||||||
} else {
|
|
||||||
throw new Parse.Error(
|
|
||||||
Parse.Error.INCORRECT_TYPE,
|
|
||||||
'schema mismatch for ' + className + '.' + key +
|
|
||||||
'; expected ' + expected + ' but got ' + type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (freeze) {
|
|
||||||
throw new Parse.Error(Parse.Error.INVALID_JSON,
|
|
||||||
'schema is frozen, cannot add ' + key + ' field');
|
|
||||||
}
|
|
||||||
|
|
||||||
// We don't have this field, but if the value is null or undefined,
|
|
||||||
// we won't update the schema until we get a value with a type.
|
|
||||||
if (!type) {
|
|
||||||
return Promise.resolve(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === 'geopoint') {
|
|
||||||
// Make sure there are not other geopoint fields
|
|
||||||
for (var otherKey in this.data[className]) {
|
|
||||||
if (this.data[className][otherKey] === 'geopoint') {
|
|
||||||
throw new Parse.Error(
|
|
||||||
Parse.Error.INCORRECT_TYPE,
|
|
||||||
'there can only be one geopoint field in a class');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We don't have this field. Update the schema.
|
|
||||||
// Note that we use the $exists guard and $set to avoid race
|
|
||||||
// conditions in the database. This is important!
|
|
||||||
var query = {_id: className};
|
|
||||||
query[key] = {'$exists': false};
|
|
||||||
var update = {};
|
|
||||||
update[key] = type;
|
|
||||||
update = {'$set': update};
|
|
||||||
return this.collection.findAndModify(query, {}, update, {}).then(() => {
|
|
||||||
// The update succeeded. Reload the schema
|
|
||||||
return this.reload();
|
|
||||||
}, () => {
|
|
||||||
// The update failed. This can be okay - it might have been a race
|
|
||||||
// condition where another client updated the schema in the same
|
|
||||||
// way that we wanted to. So, just reload the schema
|
|
||||||
return this.reload();
|
|
||||||
}).then((schema) => {
|
|
||||||
// Ensure that the schema now validates
|
|
||||||
return schema.validateField(className, key, type, true);
|
|
||||||
}, (error) => {
|
|
||||||
// The schema still doesn't validate. Give up
|
|
||||||
throw new Parse.Error(Parse.Error.INVALID_JSON,
|
|
||||||
'schema key will not revalidate');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Delete a field, and remove that data from all objects. This is intended
|
|
||||||
// to remove unused fields, if other writers are writing objects that include
|
|
||||||
// this field, the field may reappear. Returns a Promise that resolves with
|
|
||||||
// no object on success, or rejects with { code, error } on failure.
|
|
||||||
|
|
||||||
// Passing the database and prefix is necessary in order to drop relation collections
|
|
||||||
// and remove fields from objects. Ideally the database would belong to
|
|
||||||
// a database adapter and this function would close over it or access it via member.
|
|
||||||
Schema.prototype.deleteField = function(fieldName, className, database) {
|
|
||||||
if (!classNameIsValid(className)) {
|
|
||||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, invalidClassNameMessage(className));
|
|
||||||
}
|
|
||||||
if (!fieldNameIsValid(fieldName)) {
|
|
||||||
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `invalid field name: ${fieldName}`);
|
|
||||||
}
|
|
||||||
//Don't allow deleting the default fields.
|
|
||||||
if (!fieldNameIsValidForClass(fieldName, className)) {
|
|
||||||
throw new Parse.Error(136, `field ${fieldName} cannot be changed`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.reload()
|
|
||||||
.then(schema => {
|
|
||||||
return schema.hasClass(className)
|
|
||||||
.then(hasClass => {
|
|
||||||
if (!hasClass) {
|
|
||||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} does not exist.`);
|
|
||||||
}
|
|
||||||
if (!schema.data[className][fieldName]) {
|
|
||||||
throw new Parse.Error(255, `Field ${fieldName} does not exist, cannot delete.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (schema.data[className][fieldName].startsWith('relation<')) {
|
|
||||||
//For relations, drop the _Join table
|
|
||||||
return database.dropCollection(`_Join:${fieldName}:${className}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// for non-relations, remove all the data.
|
|
||||||
// This is necessary to ensure that the data is still gone if they add the same field.
|
|
||||||
return database.collection(className)
|
|
||||||
.then(collection => {
|
|
||||||
var mongoFieldName = schema.data[className][fieldName].startsWith('*') ? '_p_' + fieldName : fieldName;
|
|
||||||
return collection.update({}, { "$unset": { [mongoFieldName] : null } }, { multi: true });
|
|
||||||
});
|
|
||||||
})
|
|
||||||
// Save the _SCHEMA object
|
|
||||||
.then(() => this.collection.update({ _id: className }, { $unset: {[fieldName]: null }}));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Given a schema promise, construct another schema promise that
|
// Given a schema promise, construct another schema promise that
|
||||||
// validates this field once the schema loads.
|
// validates this field once the schema loads.
|
||||||
function thenValidateField(schemaPromise, className, key, type) {
|
function thenValidateField(schemaPromise, className, key, type) {
|
||||||
@@ -551,34 +664,6 @@ function thenValidateField(schemaPromise, className, key, type) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validates an object provided in REST format.
|
|
||||||
// Returns a promise that resolves to the new schema if this object is
|
|
||||||
// valid.
|
|
||||||
Schema.prototype.validateObject = function(className, object, query) {
|
|
||||||
var geocount = 0;
|
|
||||||
var promise = this.validateClassName(className);
|
|
||||||
for (var key in object) {
|
|
||||||
if (object[key] === undefined) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
var expected = getType(object[key]);
|
|
||||||
if (expected === 'geopoint') {
|
|
||||||
geocount++;
|
|
||||||
}
|
|
||||||
if (geocount > 1) {
|
|
||||||
throw new Parse.Error(
|
|
||||||
Parse.Error.INCORRECT_TYPE,
|
|
||||||
'there can only be one geopoint field in a class');
|
|
||||||
}
|
|
||||||
if (!expected) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
promise = thenValidateField(promise, className, key, expected);
|
|
||||||
}
|
|
||||||
promise = thenValidateRequiredColumns(promise, className, object, query);
|
|
||||||
return promise;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Given a schema promise, construct another schema promise that
|
// Given a schema promise, construct another schema promise that
|
||||||
// validates this field once the schema loads.
|
// validates this field once the schema loads.
|
||||||
function thenValidateRequiredColumns(schemaPromise, className, object, query) {
|
function thenValidateRequiredColumns(schemaPromise, className, object, query) {
|
||||||
@@ -587,85 +672,6 @@ function thenValidateRequiredColumns(schemaPromise, className, object, query) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validates that all the properties are set for the object
|
|
||||||
Schema.prototype.validateRequiredColumns = function(className, object, query) {
|
|
||||||
|
|
||||||
var columns = requiredColumns[className];
|
|
||||||
if (!columns || columns.length == 0) {
|
|
||||||
return Promise.resolve(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
var missingColumns = columns.filter(function(column){
|
|
||||||
if (query && query.objectId) {
|
|
||||||
if (object[column] && typeof object[column] === "object") {
|
|
||||||
// Trying to delete a required column
|
|
||||||
return object[column].__op == 'Delete';
|
|
||||||
}
|
|
||||||
// Not trying to do anything there
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return !object[column]
|
|
||||||
});
|
|
||||||
|
|
||||||
if (missingColumns.length > 0) {
|
|
||||||
throw new Parse.Error(
|
|
||||||
Parse.Error.INCORRECT_TYPE,
|
|
||||||
missingColumns[0]+' is required.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Validates an operation passes class-level-permissions set in the schema
|
|
||||||
Schema.prototype.validatePermission = function(className, aclGroup, operation) {
|
|
||||||
if (!this.perms[className] || !this.perms[className][operation]) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
var perms = this.perms[className][operation];
|
|
||||||
// Handle the public scenario quickly
|
|
||||||
if (perms['*']) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
// Check permissions against the aclGroup provided (array of userId/roles)
|
|
||||||
var found = false;
|
|
||||||
for (var i = 0; i < aclGroup.length && !found; i++) {
|
|
||||||
if (perms[aclGroup[i]]) {
|
|
||||||
found = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!found) {
|
|
||||||
// TODO: Verify correct error code
|
|
||||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
|
|
||||||
'Permission denied for this action.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Returns the expected type for a className+key combination
|
|
||||||
// or undefined if the schema is not set
|
|
||||||
Schema.prototype.getExpectedType = function(className, key) {
|
|
||||||
if (this.data && this.data[className]) {
|
|
||||||
return this.data[className][key];
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Checks if a given class is in the schema. Needs to load the
|
|
||||||
// schema first, which is kinda janky. Hopefully we can refactor
|
|
||||||
// and make this be a regular value.
|
|
||||||
Schema.prototype.hasClass = function(className) {
|
|
||||||
return this.reload().then(newSchema => !!newSchema.data[className]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to check if a field is a pointer, returns true or false.
|
|
||||||
Schema.prototype.isPointer = function(className, key) {
|
|
||||||
var expected = this.getExpectedType(className, key);
|
|
||||||
if (expected && expected.charAt(0) == '*') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Gets the type from a REST API formatted object, where 'type' is
|
// Gets the type from a REST API formatted object, where 'type' is
|
||||||
// extended past javascript types to include the rest of the Parse
|
// extended past javascript types to include the rest of the Parse
|
||||||
// type system.
|
// type system.
|
||||||
@@ -674,21 +680,21 @@ Schema.prototype.isPointer = function(className, key) {
|
|||||||
function getType(obj) {
|
function getType(obj) {
|
||||||
var type = typeof obj;
|
var type = typeof obj;
|
||||||
switch(type) {
|
switch(type) {
|
||||||
case 'boolean':
|
case 'boolean':
|
||||||
case 'string':
|
case 'string':
|
||||||
case 'number':
|
case 'number':
|
||||||
return type;
|
return type;
|
||||||
case 'map':
|
case 'map':
|
||||||
case 'object':
|
case 'object':
|
||||||
if (!obj) {
|
if (!obj) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return getObjectType(obj);
|
return getObjectType(obj);
|
||||||
case 'function':
|
case 'function':
|
||||||
case 'symbol':
|
case 'symbol':
|
||||||
case 'undefined':
|
case 'undefined':
|
||||||
default:
|
default:
|
||||||
throw 'bad obj: ' + obj;
|
throw 'bad obj: ' + obj;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -730,27 +736,26 @@ function getObjectType(obj) {
|
|||||||
}
|
}
|
||||||
if (obj.__op) {
|
if (obj.__op) {
|
||||||
switch(obj.__op) {
|
switch(obj.__op) {
|
||||||
case 'Increment':
|
case 'Increment':
|
||||||
return 'number';
|
return 'number';
|
||||||
case 'Delete':
|
case 'Delete':
|
||||||
return null;
|
return null;
|
||||||
case 'Add':
|
case 'Add':
|
||||||
case 'AddUnique':
|
case 'AddUnique':
|
||||||
case 'Remove':
|
case 'Remove':
|
||||||
return 'array';
|
return 'array';
|
||||||
case 'AddRelation':
|
case 'AddRelation':
|
||||||
case 'RemoveRelation':
|
case 'RemoveRelation':
|
||||||
return 'relation<' + obj.objects[0].className + '>';
|
return 'relation<' + obj.objects[0].className + '>';
|
||||||
case 'Batch':
|
case 'Batch':
|
||||||
return getObjectType(obj.ops[0]);
|
return getObjectType(obj.ops[0]);
|
||||||
default:
|
default:
|
||||||
throw 'unexpected op: ' + obj.__op;
|
throw 'unexpected op: ' + obj.__op;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 'object';
|
return 'object';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
load: load,
|
load: load,
|
||||||
classNameIsValid: classNameIsValid,
|
classNameIsValid: classNameIsValid,
|
||||||
|
|||||||
24
src/index.js
24
src/index.js
@@ -134,7 +134,8 @@ function ParseServer({
|
|||||||
const filesControllerAdapter = loadAdapter(filesAdapter, () => {
|
const filesControllerAdapter = loadAdapter(filesAdapter, () => {
|
||||||
return new GridStoreAdapter(databaseURI);
|
return new GridStoreAdapter(databaseURI);
|
||||||
});
|
});
|
||||||
const pushControllerAdapter = loadAdapter(push, ParsePushAdapter);
|
// Pass the push options too as it works with the default
|
||||||
|
const pushControllerAdapter = loadAdapter(push && push.adapter, ParsePushAdapter, push);
|
||||||
const loggerControllerAdapter = loadAdapter(loggerAdapter, FileLoggerAdapter);
|
const loggerControllerAdapter = loadAdapter(loggerAdapter, FileLoggerAdapter);
|
||||||
const emailControllerAdapter = loadAdapter(emailAdapter);
|
const emailControllerAdapter = loadAdapter(emailAdapter);
|
||||||
// We pass the options and the base class for the adatper,
|
// We pass the options and the base class for the adatper,
|
||||||
@@ -233,15 +234,18 @@ function ParseServer({
|
|||||||
|
|
||||||
api.use(middlewares.handleParseErrors);
|
api.use(middlewares.handleParseErrors);
|
||||||
|
|
||||||
process.on('uncaughtException', (err) => {
|
//This causes tests to spew some useless warnings, so disable in test
|
||||||
if( err.code === "EADDRINUSE" ) { // user-friendly message for this common error
|
if (!process.env.TESTING) {
|
||||||
console.log(`Unable to listen on port ${err.port}. The port is already in use.`);
|
process.on('uncaughtException', (err) => {
|
||||||
process.exit(0);
|
if( err.code === "EADDRINUSE" ) { // user-friendly message for this common error
|
||||||
}
|
console.log(`Unable to listen on port ${err.port}. The port is already in use.`);
|
||||||
else {
|
process.exit(0);
|
||||||
throw err;
|
}
|
||||||
}
|
else {
|
||||||
});
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
hooksController.load();
|
hooksController.load();
|
||||||
|
|
||||||
return api;
|
return api;
|
||||||
|
|||||||
Reference in New Issue
Block a user