mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e60901de6a | ||
|
|
b2f36e192a | ||
|
|
10d2988985 | ||
|
|
37c502bed0 | ||
|
|
b00572de65 | ||
|
|
da5b5ccefa | ||
|
|
7c850fc1ad | ||
|
|
90df7841ef | ||
|
|
0b88302bea | ||
|
|
583bb34995 | ||
|
|
71ae7bee94 | ||
|
|
449ca115b1 | ||
|
|
d4bd21fcbc | ||
|
|
a926712951 | ||
|
|
5f564f32f5 | ||
|
|
874d10fc74 | ||
|
|
4371ca164c | ||
|
|
608cba9e8c | ||
|
|
5cbf3eb8dd | ||
|
|
37953d146b | ||
|
|
a3179e43fb | ||
|
|
7b431ad064 | ||
|
|
3d7589c1bc | ||
|
|
ee8a3c94d5 | ||
|
|
e2a3213351 | ||
|
|
5c7b346292 | ||
|
|
0542f860f4 | ||
|
|
e0a9f19895 | ||
|
|
21e73ac276 | ||
|
|
957b5927b1 | ||
|
|
234d0093ff | ||
|
|
d14d451028 | ||
|
|
d33dd68cc5 | ||
|
|
0d094767cf | ||
|
|
ab827e3c2f | ||
|
|
d49d539a81 | ||
|
|
6e9529f81c | ||
|
|
2d94a885d2 | ||
|
|
b7b57f7ea7 | ||
|
|
b185508508 | ||
|
|
7b91ee98ed | ||
|
|
9776362ab2 | ||
|
|
59b4047de8 | ||
|
|
54b21c2f73 | ||
|
|
5d37755d88 | ||
|
|
ddd01181be | ||
|
|
bf47a0b2b6 | ||
|
|
3945e0c680 | ||
|
|
61b62e4e9f | ||
|
|
0708af17d7 | ||
|
|
cecb2a1cb1 | ||
|
|
ab0ea09067 | ||
|
|
414ee6740d | ||
|
|
65bd59d95f | ||
|
|
ac839f75c7 | ||
|
|
c57c6b33b1 |
@@ -1,19 +1,30 @@
|
||||
Check out [this issue](https://github.com/ParsePlatform/parse-server/issues/1271) for an ideal bug report. The closer your issue report is to that one, the more likely we are to be able to help, and the more likely we will be to fix the issue quickly!
|
||||
Check out https://github.com/ParsePlatform/parse-server/issues/1271 for an ideal bug report. The closer your issue report is to that one, the more likely we are to be able to help, and the more likely we will be to fix the issue quickly!
|
||||
|
||||
For implementation related questions or technical support, please refer to the [Stack Overflow](http://stackoverflow.com/questions/tagged/parse.com) and [Server Fault](https://serverfault.com/tags/parse) communities.
|
||||
Many members of the community use Stack Overflow and Server Fault to ask questions. Read through the existing questions or ask your own!
|
||||
|
||||
- Stack Overflow: http://stackoverflow.com/questions/tagged/parse.com
|
||||
- Server Fault: https://serverfault.com/tags/parse
|
||||
|
||||
For database migration help, please file a bug report at https://parse.com/help#report
|
||||
|
||||
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 of Parse Server: https://github.com/ParsePlatform/parse-server/releases
|
||||
|
||||
- [ ] 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=is%3Aissue Chances are that your issue has been reported or resolved before.
|
||||
|
||||
#### Environment Setup
|
||||
|
||||
- Server: parse-server version, operating system, hardware, local or remote?
|
||||
- Database: version, storage engine, hardware, local or remote?
|
||||
|
||||
#### Steps to reproduce
|
||||
|
||||
- Can this issue be reproduced using the Parse Server REST API? Include curl commands when applicable.
|
||||
- What was the expected result? What is the actual outcome?
|
||||
|
||||
#### Logs/Trace
|
||||
|
||||
- You can turn on additional logging by configuring VERBOSE=1 in your environment.
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
## Parse Server Changelog
|
||||
|
||||
### 2.2.8 (5/8/2016)
|
||||
|
||||
* New: Support for Pointer Permissions
|
||||
* New: Expose logger in Cloud Code
|
||||
* New: Option to revoke sessions on password reset
|
||||
* New: Option to expire inactive sessions
|
||||
* Perf: Improvements in ACL checking query
|
||||
* Fix: Issues when sending pushes to list of devices that contains invalid values
|
||||
* Fix: Issues caused by using babel-polyfill outside of Parse Server, but in the same express app
|
||||
* Fix: Remove creation of extra session tokens
|
||||
* Fix: Return authData when querying with master key
|
||||
* Fix: Bugs when deleting webhooks
|
||||
* Fix: Ignore _RevocableSession header, which might be sent by the JS SDK
|
||||
* Fix: Issues with querying via URL params
|
||||
* Fix: Properly encode "Date" parameters to cloud code functions
|
||||
|
||||
|
||||
### 2.2.7 (4/15/2016)
|
||||
|
||||
* Adds support for --verbose and verbose option when running ParseServer [\#1414](https://github.com/ParsePlatform/parse-server/pull/1414) ([flovilmart](https://github.com/flovilmart))
|
||||
|
||||
@@ -10,6 +10,8 @@ Parse Server works with the Express web application framework. It can be added t
|
||||
|
||||
# Getting Started
|
||||
|
||||
April 2016 - We created a series of video screencasts, please check them out here: [http://blog.parse.com/learn/parse-server-video-series-april-2016/](http://blog.parse.com/learn/parse-server-video-series-april-2016/)
|
||||
|
||||
The fastest and easiest way to get started is to run MongoDB and Parse Server locally.
|
||||
|
||||
## Running Parse Server locally
|
||||
@@ -147,7 +149,7 @@ The full documentation for Parse Server is available in the [wiki](https://githu
|
||||
|
||||
## Migrating an Existing Parse App
|
||||
|
||||
The hosted version of Parse will be fully retired on January 28th, 2017. If you are planning to migrate an app, you need to begin work as soon as possible. There are a few areas where Parse Server does not provide compatibility with the hosted version of Parse. Learn more in the [Migration guide](https://github.com/ParsePlatform/parse-server/wiki/Migrating-an-Existing-Parse-App).
|
||||
The hosted version of Parse will be fully retired on January 28th, 2017. If you are planning to migrate an app, you need to begin work as soon as possible. There are a few areas where Parse Server does not provide compatibility with the hosted version of Parse. Learn more in the [Migration guide](https://parse.com/migration).
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -159,7 +161,7 @@ For the full list of available options, run `parse-server --help`.
|
||||
|
||||
* `appId` **(required)** - The application id to host with this server instance. You can use any arbitrary string. For migrated apps, this should match your hosted Parse app.
|
||||
* `masterKey` **(required)** - The master key to use for overriding ACL security. You can use any arbitrary string. Keep it secret! For migrated apps, this should match your hosted Parse app.
|
||||
* `databaseURI` **(required)** - The connection string for your database, i.e. `mongodb://user:pass@host.com/dbname`. Be sure to [URL encode your password](https://app.zencoder.com/docs/guides/getting-started/special-characters-in-usernames-and-passwords) if your password has special charachters.
|
||||
* `databaseURI` **(required)** - The connection string for your database, i.e. `mongodb://user:pass@host.com/dbname`. Be sure to [URL encode your password](https://app.zencoder.com/docs/guides/getting-started/special-characters-in-usernames-and-passwords) if your password has special characters.
|
||||
* `port` - The default port is 1337, specify this parameter to use a different port.
|
||||
* `serverURL` - URL to your Parse Server (don't forget to specify http:// or https://). This URL will be used when making requests to Parse Server from Cloud Code.
|
||||
* `cloud` - The absolute path to your cloud code `main.js` file.
|
||||
@@ -186,6 +188,7 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo
|
||||
* `maxUploadSize` - Max file size for uploads. Defaults to 20 MB.
|
||||
* `loggerAdapter` - The default behavior/transport (File) can be changed by creating an adapter class (see [`LoggerAdapter.js`](https://github.com/ParsePlatform/parse-server/blob/master/src/Adapters/Logger/LoggerAdapter.js)).
|
||||
* `sessionLength` - The length of time in seconds that a session should be valid for. Defaults to 31536000 seconds (1 year).
|
||||
* `revokeSessionOnPasswordReset` - When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions.
|
||||
|
||||
##### Email verification and password reset
|
||||
|
||||
@@ -217,7 +220,7 @@ var server = ParseServer({
|
||||
});
|
||||
```
|
||||
|
||||
You can also use other email adapters contributed by the community such as [parse-server-sendgrid-adapter](https://www.npmjs.com/package/parse-server-sendgrid-adapter).
|
||||
You can also use other email adapters contributed by the community such as [parse-server-sendgrid-adapter](https://www.npmjs.com/package/parse-server-sendgrid-adapter) or [parse-server-mandrill-adapter](https://github.com/back4app/parse-server-mandrill-adapter).
|
||||
|
||||
### Using environment variables to configure Parse Server
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "parse-server",
|
||||
"version": "2.2.7",
|
||||
"version": "2.2.8",
|
||||
"description": "An express module providing a Parse-compatible API server",
|
||||
"main": "lib/index.js",
|
||||
"repository": {
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
'use strict';
|
||||
var LoggerController = require('../src/Controllers/LoggerController').LoggerController;
|
||||
var FileLoggerAdapter = require('../src/Adapters/Logger/FileLoggerAdapter').FileLoggerAdapter;
|
||||
|
||||
describe("Cloud Code Logger", () => {
|
||||
|
||||
it("should expose log to functions", (done) => {
|
||||
var logController = new LoggerController(new FileLoggerAdapter());
|
||||
|
||||
Parse.Cloud.define("loggerTest", (req, res) => {
|
||||
req.log.info('logTest', 'info log', {info: 'some log' });
|
||||
req.log.error('logTest','error log', {error: 'there was an error'});
|
||||
res.success({});
|
||||
});
|
||||
|
||||
Parse.Cloud.run('loggerTest').then(() => {
|
||||
Parse.Cloud._removeHook('Functions', 'logTest');
|
||||
return logController.getLogs({from: Date.now() - 500, size: 1000});
|
||||
}).then((res) => {
|
||||
expect(res.length).not.toBe(0);
|
||||
let lastLogs = res.slice(0, 2);
|
||||
let errorMessage = lastLogs[0];
|
||||
let infoMessage = lastLogs[1];
|
||||
expect(errorMessage.level).toBe('error');
|
||||
expect(errorMessage.error).toBe('there was an error');
|
||||
expect(errorMessage.message).toBe('logTest error log');
|
||||
expect(infoMessage.level).toBe('info');
|
||||
expect(infoMessage.info).toBe('some log');
|
||||
expect(infoMessage.message).toBe('logTest info log');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should expose log to trigger", (done) => {
|
||||
var logController = new LoggerController(new FileLoggerAdapter());
|
||||
|
||||
Parse.Cloud.beforeSave("MyObject", (req, res) => {
|
||||
req.log.info('beforeSave MyObject', 'info log', {info: 'some log' });
|
||||
req.log.error('beforeSave MyObject','error log', {error: 'there was an error'});
|
||||
res.success({});
|
||||
});
|
||||
|
||||
let obj = new Parse.Object('MyObject');
|
||||
obj.save().then(() => {
|
||||
Parse.Cloud._removeHook('Triggers', 'beforeSave', 'MyObject');
|
||||
return logController.getLogs({from: Date.now() - 500, size: 1000})
|
||||
}).then((res) => {
|
||||
expect(res.length).not.toBe(0);
|
||||
let lastLogs = res.slice(0, 2);
|
||||
let errorMessage = lastLogs[0];
|
||||
let infoMessage = lastLogs[1];
|
||||
expect(errorMessage.level).toBe('error');
|
||||
expect(errorMessage.error).toBe('there was an error');
|
||||
expect(errorMessage.message).toBe('beforeSave MyObject error log');
|
||||
expect(infoMessage.level).toBe('info');
|
||||
expect(infoMessage.info).toBe('some log');
|
||||
expect(infoMessage.message).toBe('beforeSave MyObject info log');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,8 @@ describe('info logs', () => {
|
||||
var fileLoggerAdapter = new FileLoggerAdapter();
|
||||
fileLoggerAdapter.info('testing info logs', () => {
|
||||
fileLoggerAdapter.query({
|
||||
size: 1,
|
||||
from: new Date(Date.now() - 500),
|
||||
size: 100,
|
||||
level: 'info'
|
||||
}, (results) => {
|
||||
if(results.length == 0) {
|
||||
@@ -28,7 +29,8 @@ describe('error logs', () => {
|
||||
var fileLoggerAdapter = new FileLoggerAdapter();
|
||||
fileLoggerAdapter.error('testing error logs', () => {
|
||||
fileLoggerAdapter.query({
|
||||
size: 1,
|
||||
from: new Date(Date.now() - 500),
|
||||
size: 100,
|
||||
level: 'error'
|
||||
}, (results) => {
|
||||
if(results.length == 0) {
|
||||
|
||||
@@ -216,17 +216,6 @@ describe("httpRequest", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should get a cat image', (done) => {
|
||||
httpRequest({
|
||||
url: 'http://thecatapi.com/api/images/get?format=src&type=jpg',
|
||||
followRedirects: true
|
||||
}).then((res) => {
|
||||
expect(res.buffer).not.toBe(null);
|
||||
expect(res.text).not.toBe(null);
|
||||
done();
|
||||
})
|
||||
})
|
||||
|
||||
it("should params object to query string", (done) => {
|
||||
httpRequest({
|
||||
url: httpRequestServer+"/qs",
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
var auth = require('../src/Auth');
|
||||
var Config = require('../src/Config');
|
||||
var rest = require('../src/rest');
|
||||
var InstallationsRouter = require('../src/Routers/InstallationsRouter').InstallationsRouter;
|
||||
|
||||
var config = new Config('test');
|
||||
|
||||
describe('InstallationsRouter', () => {
|
||||
it('uses find condition from request.body', (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: {
|
||||
where: {
|
||||
deviceType: 'android'
|
||||
}
|
||||
},
|
||||
query: {}
|
||||
};
|
||||
|
||||
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 results = res.response.results;
|
||||
expect(results.length).toEqual(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('uses find condition from request.query', (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: {
|
||||
where: {
|
||||
deviceType: 'android'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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 results = res.response.results;
|
||||
expect(results.length).toEqual(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
+31
-15
@@ -23,11 +23,13 @@ var dummySchema = {
|
||||
};
|
||||
|
||||
|
||||
describe('transformCreate', () => {
|
||||
describe('parseObjectToMongoObjectForCreate', () => {
|
||||
|
||||
it('a basic number', (done) => {
|
||||
var input = {five: 5};
|
||||
var output = transform.transformCreate(dummySchema, null, input);
|
||||
var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input, {
|
||||
fields: {five: {type: 'Number'}}
|
||||
});
|
||||
jequal(input, output);
|
||||
done();
|
||||
});
|
||||
@@ -37,7 +39,7 @@ describe('transformCreate', () => {
|
||||
createdAt: "2015-10-06T21:24:50.332Z",
|
||||
updatedAt: "2015-10-06T21:24:50.332Z"
|
||||
};
|
||||
var output = transform.transformCreate(dummySchema, null, input);
|
||||
var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input);
|
||||
expect(output._created_at instanceof Date).toBe(true);
|
||||
expect(output._updated_at instanceof Date).toBe(true);
|
||||
done();
|
||||
@@ -49,21 +51,25 @@ describe('transformCreate', () => {
|
||||
objectId: 'myId',
|
||||
className: 'Blah',
|
||||
};
|
||||
var out = transform.transformCreate(dummySchema, null, {pointers: [pointer]});
|
||||
var out = transform.parseObjectToMongoObjectForCreate(dummySchema, null, {pointers: [pointer]},{
|
||||
fields: {pointers: {type: 'Array'}}
|
||||
});
|
||||
jequal([pointer], out.pointers);
|
||||
done();
|
||||
});
|
||||
|
||||
it('a delete op', (done) => {
|
||||
//TODO: object creation requests shouldn't be seeing __op delete, it makes no sense to
|
||||
//have __op delete in a new object. Figure out what this should actually be testing.
|
||||
notWorking('a delete op', (done) => {
|
||||
var input = {deleteMe: {__op: 'Delete'}};
|
||||
var output = transform.transformCreate(dummySchema, null, input);
|
||||
var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input);
|
||||
jequal(output, {});
|
||||
done();
|
||||
});
|
||||
|
||||
it('basic ACL', (done) => {
|
||||
var input = {ACL: {'0123': {'read': true, 'write': true}}};
|
||||
var output = transform.transformCreate(dummySchema, null, input);
|
||||
var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input);
|
||||
// This just checks that it doesn't crash, but it should check format.
|
||||
done();
|
||||
});
|
||||
@@ -71,21 +77,27 @@ describe('transformCreate', () => {
|
||||
describe('GeoPoints', () => {
|
||||
it('plain', (done) => {
|
||||
var geoPoint = {__type: 'GeoPoint', longitude: 180, latitude: -180};
|
||||
var out = transform.transformCreate(dummySchema, null, {location: geoPoint});
|
||||
var out = transform.parseObjectToMongoObjectForCreate(dummySchema, null, {location: geoPoint},{
|
||||
fields: {location: {type: 'GeoPoint'}}
|
||||
});
|
||||
expect(out.location).toEqual([180, -180]);
|
||||
done();
|
||||
});
|
||||
|
||||
it('in array', (done) => {
|
||||
var geoPoint = {__type: 'GeoPoint', longitude: 180, latitude: -180};
|
||||
var out = transform.transformCreate(dummySchema, null, {locations: [geoPoint, geoPoint]});
|
||||
var out = transform.parseObjectToMongoObjectForCreate(dummySchema, null, {locations: [geoPoint, geoPoint]},{
|
||||
fields: {locations: {type: 'Array'}}
|
||||
});
|
||||
expect(out.locations).toEqual([geoPoint, geoPoint]);
|
||||
done();
|
||||
});
|
||||
|
||||
it('in sub-object', (done) => {
|
||||
var geoPoint = {__type: 'GeoPoint', longitude: 180, latitude: -180};
|
||||
var out = transform.transformCreate(dummySchema, null, { locations: { start: geoPoint }});
|
||||
var out = transform.parseObjectToMongoObjectForCreate(dummySchema, null, { locations: { start: geoPoint }},{
|
||||
fields: {locations: {type: 'Object'}}
|
||||
});
|
||||
expect(out).toEqual({ locations: { start: geoPoint } });
|
||||
done();
|
||||
});
|
||||
@@ -94,7 +106,7 @@ describe('transformCreate', () => {
|
||||
|
||||
describe('transformWhere', () => {
|
||||
it('objectId', (done) => {
|
||||
var out = transform.transformWhere(dummySchema, null, {objectId: 'foo'});
|
||||
var out = transform.transformWhere(null, {objectId: 'foo'});
|
||||
expect(out._id).toEqual('foo');
|
||||
done();
|
||||
});
|
||||
@@ -103,7 +115,7 @@ describe('transformWhere', () => {
|
||||
var input = {
|
||||
objectId: {'$in': ['one', 'two', 'three']},
|
||||
};
|
||||
var output = transform.transformWhere(dummySchema, null, input);
|
||||
var output = transform.transformWhere(null, input);
|
||||
jequal(input.objectId, output._id);
|
||||
done();
|
||||
});
|
||||
@@ -196,7 +208,9 @@ describe('transform schema key changes', () => {
|
||||
var input = {
|
||||
somePointer: {__type: 'Pointer', className: 'Micro', objectId: 'oft'}
|
||||
};
|
||||
var output = transform.transformCreate(dummySchema, null, input);
|
||||
var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input, {
|
||||
fields: {somePointer: {type: 'Pointer'}}
|
||||
});
|
||||
expect(typeof output._p_somePointer).toEqual('string');
|
||||
expect(output._p_somePointer).toEqual('Micro$oft');
|
||||
done();
|
||||
@@ -206,7 +220,9 @@ describe('transform schema key changes', () => {
|
||||
var input = {
|
||||
userPointer: {__type: 'Pointer', className: '_User', objectId: 'qwerty'}
|
||||
};
|
||||
var output = transform.transformCreate(dummySchema, null, input);
|
||||
var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input, {
|
||||
fields: {userPointer: {type: 'Pointer'}}
|
||||
});
|
||||
expect(typeof output._p_userPointer).toEqual('string');
|
||||
expect(output._p_userPointer).toEqual('_User$qwerty');
|
||||
done();
|
||||
@@ -219,7 +235,7 @@ describe('transform schema key changes', () => {
|
||||
"Kevin": { "write": true }
|
||||
}
|
||||
};
|
||||
var output = transform.transformCreate(dummySchema, null, input);
|
||||
var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input);
|
||||
expect(typeof output._rperm).toEqual('object');
|
||||
expect(typeof output._wperm).toEqual('object');
|
||||
expect(output.ACL).toBeUndefined();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
let request = require('request');
|
||||
|
||||
describe('Parse.Push', () => {
|
||||
|
||||
@@ -89,4 +90,57 @@ describe('Parse.Push', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not allow clients to query _PushStatus', done => {
|
||||
setup()
|
||||
.then(() => Parse.Push.send({
|
||||
where: {
|
||||
deviceType: 'ios'
|
||||
},
|
||||
data: {
|
||||
badge: 'increment',
|
||||
alert: 'Hello world!'
|
||||
}
|
||||
}, {useMasterKey: true}))
|
||||
.then(() => {
|
||||
request.get({
|
||||
url: 'http://localhost:8378/1/classes/_PushStatus',
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
},
|
||||
}, (error, response, body) => {
|
||||
expect(body.results.length).toEqual(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow master key to query _PushStatus', done => {
|
||||
setup()
|
||||
.then(() => Parse.Push.send({
|
||||
where: {
|
||||
deviceType: 'ios'
|
||||
},
|
||||
data: {
|
||||
badge: 'increment',
|
||||
alert: 'Hello world!'
|
||||
}
|
||||
}, {useMasterKey: true}))
|
||||
.then(() => {
|
||||
request.get({
|
||||
url: 'http://localhost:8378/1/classes/_PushStatus',
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
},
|
||||
}, (error, response, body) => {
|
||||
expect(body.results.length).toEqual(1);
|
||||
expect(body.results[0].query).toEqual('{"deviceType":"ios"}');
|
||||
expect(body.results[0].payload).toEqual('{"badge":"increment","alert":"Hello world!"}');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -169,11 +169,11 @@ describe('Parse.ACL', () => {
|
||||
ok(object.get("ACL"));
|
||||
|
||||
// Start making requests by the public, which should all fail.
|
||||
Parse.User.logOut();
|
||||
|
||||
// Delete
|
||||
object.destroy().then(() => {
|
||||
Parse.User.logOut()
|
||||
.then(() => object.destroy())
|
||||
.then(() => {
|
||||
fail('destroy should fail');
|
||||
done();
|
||||
}, error => {
|
||||
expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND);
|
||||
done();
|
||||
|
||||
+72
-1
@@ -39,8 +39,8 @@ describe('miscellaneous', function() {
|
||||
expect(data.get('password')).toBeUndefined();
|
||||
done();
|
||||
}, function(err) {
|
||||
console.log(err);
|
||||
fail(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1298,6 +1298,77 @@ describe('miscellaneous', function() {
|
||||
done();
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
it('properly returns incremented values (#1554)', (done) => {
|
||||
let headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest'
|
||||
};
|
||||
let requestOptions = {
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/classes/AnObject',
|
||||
json: true
|
||||
};
|
||||
let object = new Parse.Object('AnObject');;
|
||||
|
||||
function runIncrement(amount) {
|
||||
let options = Object.assign({}, requestOptions, {
|
||||
body: {
|
||||
"key": {
|
||||
__op: 'Increment',
|
||||
amount: amount
|
||||
}
|
||||
},
|
||||
url: 'http://localhost:8378/1/classes/AnObject/'+object.id
|
||||
})
|
||||
return new Promise((resolve, reject) => {
|
||||
request.put(options, (err, res, body) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(body);
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
object.save().then(() => {
|
||||
return runIncrement(1);
|
||||
}).then((res) => {
|
||||
expect(res.key).toBe(1);
|
||||
return runIncrement(-1);
|
||||
}).then((res) => {
|
||||
expect(res.key).toBe(0);
|
||||
done();
|
||||
})
|
||||
})
|
||||
|
||||
it('ignores _RevocableSession "header" send by JS SDK', (done) => {
|
||||
let object = new Parse.Object('AnObject');
|
||||
object.set('a', 'b');
|
||||
object.save().then(() => {
|
||||
request.post({
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
url: 'http://localhost:8378/1/classes/AnObject',
|
||||
body: {
|
||||
_method: 'GET',
|
||||
_ApplicationId: 'test',
|
||||
_JavaScriptKey: 'test',
|
||||
_ClientVersion: 'js1.8.3',
|
||||
_InstallationId: 'iid',
|
||||
_RevocableSession: "1",
|
||||
},
|
||||
json: true
|
||||
}, (err, res, body) => {
|
||||
expect(body.error).toBeUndefined();
|
||||
expect(body.results).not.toBeUndefined();
|
||||
expect(body.results.length).toBe(1);
|
||||
let result = body.results[0];
|
||||
expect(result.a).toBe('b');
|
||||
done();
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -85,8 +85,8 @@ describe('Parse.GeoPoint testing', () => {
|
||||
equal(results.length, 3);
|
||||
done();
|
||||
}, (err) => {
|
||||
console.log(err);
|
||||
fail();
|
||||
fail("Couldn't query GeoPoint");
|
||||
fail(err)
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+39
-45
@@ -37,52 +37,46 @@ describe('Hooks', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should CRUD a function registration", (done) => {
|
||||
// Create
|
||||
Parse.Hooks.createFunction("My-Test-Function", "http://someurl").then((res) => {
|
||||
expect(res.functionName).toBe("My-Test-Function");
|
||||
expect(res.url).toBe("http://someurl")
|
||||
// Find
|
||||
return Parse.Hooks.getFunction("My-Test-Function");
|
||||
}, (err) => {
|
||||
fail(err);
|
||||
done();
|
||||
}).then((res) => {
|
||||
expect(res).not.toBe(null);
|
||||
expect(res).not.toBe(undefined);
|
||||
expect(res.url).toBe("http://someurl");
|
||||
// delete
|
||||
return Parse.Hooks.updateFunction("My-Test-Function", "http://anotherurl");
|
||||
}, (err) => {
|
||||
fail(err);
|
||||
done();
|
||||
}).then((res) => {
|
||||
expect(res.functionName).toBe("My-Test-Function");
|
||||
expect(res.url).toBe("http://anotherurl")
|
||||
it("should CRUD a function registration", (done) => {
|
||||
// Create
|
||||
Parse.Hooks.createFunction("My-Test-Function", "http://someurl")
|
||||
.then(response => {
|
||||
expect(response.functionName).toBe("My-Test-Function");
|
||||
expect(response.url).toBe("http://someurl")
|
||||
// Find
|
||||
return Parse.Hooks.getFunction("My-Test-Function")
|
||||
}).then(response => {
|
||||
expect(response.url).toBe("http://someurl");
|
||||
return Parse.Hooks.updateFunction("My-Test-Function", "http://anotherurl");
|
||||
})
|
||||
.then((res) => {
|
||||
expect(res.functionName).toBe("My-Test-Function");
|
||||
expect(res.url).toBe("http://anotherurl")
|
||||
// delete
|
||||
return Parse.Hooks.deleteFunction("My-Test-Function")
|
||||
})
|
||||
.then((res) => {
|
||||
// Find again! but should be deleted
|
||||
return Parse.Hooks.getFunction("My-Test-Function")
|
||||
.then(res => {
|
||||
fail("Failed to delete hook")
|
||||
fail(res)
|
||||
done();
|
||||
return Promise.resolve();
|
||||
}, (err) => {
|
||||
expect(err.code).toBe(143);
|
||||
expect(err.error).toBe("no function named: My-Test-Function is defined")
|
||||
done();
|
||||
return Promise.resolve();
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
fail(error);
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
return Parse.Hooks.deleteFunction("My-Test-Function");
|
||||
}, (err) => {
|
||||
fail(err);
|
||||
done();
|
||||
}).then((res) => {
|
||||
// Find again! but should be deleted
|
||||
return Parse.Hooks.getFunction("My-Test-Function");
|
||||
}, (err) => {
|
||||
fail(err);
|
||||
done();
|
||||
}).then((res) => {
|
||||
fail("Should not succeed")
|
||||
done();
|
||||
}, (err) => {
|
||||
expect(err).not.toBe(null);
|
||||
expect(err).not.toBe(undefined);
|
||||
expect(err.code).toBe(143);
|
||||
expect(err.error).toBe("no function named: My-Test-Function is defined")
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it("should CRUD a trigger registration", (done) => {
|
||||
it("should CRUD a trigger registration", (done) => {
|
||||
// Create
|
||||
Parse.Hooks.createTrigger("MyClass","beforeDelete", "http://someurl").then((res) => {
|
||||
expect(res.className).toBe("MyClass");
|
||||
|
||||
@@ -683,4 +683,62 @@ describe('Parse.Relation testing', () => {
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
it('can query roles in Cloud Code (regession test #1489)', done => {
|
||||
Parse.Cloud.define('isAdmin', (request, response) => {
|
||||
let query = new Parse.Query(Parse.Role);
|
||||
query.equalTo('name', 'admin');
|
||||
query.first({ useMasterKey: true })
|
||||
.then(role => {
|
||||
let relation = new Parse.Relation(role, 'users');
|
||||
let admins = relation.query();
|
||||
admins.equalTo('username', request.user.get('username'));
|
||||
admins.first({ useMasterKey: true })
|
||||
.then(user => {
|
||||
if (user) {
|
||||
Parse.Cloud._removeHook('Functions', 'isAdmin');
|
||||
done();
|
||||
} else {
|
||||
Parse.Cloud._removeHook('Functions', 'isAdmin');
|
||||
fail('Should have found admin user, found nothing instead');
|
||||
done();
|
||||
}
|
||||
}, error => {
|
||||
Parse.Cloud._removeHook('Functions', 'isAdmin');
|
||||
fail('User not admin');
|
||||
done();
|
||||
})
|
||||
}, error => {
|
||||
Parse.Cloud._removeHook('Functions', 'isAdmin');
|
||||
fail('Should have found admin user, errored instead');
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
let adminUser = new Parse.User();
|
||||
adminUser.set('username', 'name');
|
||||
adminUser.set('password', 'pass');
|
||||
adminUser.signUp()
|
||||
.then(adminUser => {
|
||||
let adminACL = new Parse.ACL();
|
||||
adminACL.setPublicReadAccess(true);
|
||||
|
||||
// Create admin role
|
||||
let adminRole = new Parse.Role('admin', adminACL);
|
||||
adminRole.getUsers().add(adminUser);
|
||||
adminRole.save()
|
||||
.then(() => {
|
||||
Parse.Cloud.run('isAdmin');
|
||||
}, error => {
|
||||
fail('failed to save role');
|
||||
fail(error);
|
||||
done()
|
||||
});
|
||||
}, error => {
|
||||
fail('failed to sign up');
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+139
-5
@@ -1028,6 +1028,23 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns authData when authed and logged in with provider (regression test for #1498)', done => {
|
||||
Parse.Object.enableSingleInstance();
|
||||
let provider = getMockFacebookProvider();
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
Parse.User._logInWith('facebook', {
|
||||
success: user => {
|
||||
let userQuery = new Parse.Query(Parse.User);
|
||||
userQuery.get(user.id)
|
||||
.then(user => {
|
||||
expect(user.get('authData')).not.toBeUndefined();
|
||||
Parse.Object.disableSingleInstance();
|
||||
done();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('log in with provider with files', done => {
|
||||
let provider = getMockFacebookProvider();
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
@@ -2098,9 +2115,7 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Sometimes the authData still has null on that keys
|
||||
// https://github.com/ParsePlatform/parse-server/issues/935
|
||||
it('should cleanup null authData keys', (done) => {
|
||||
it('should cleanup null authData keys (regression test for #935)', (done) => {
|
||||
let database = new Config(Parse.applicationId).database;
|
||||
database.create('_User', {
|
||||
username: 'user',
|
||||
@@ -2134,8 +2149,7 @@ describe('Parse.User testing', () => {
|
||||
})
|
||||
});
|
||||
|
||||
// https://github.com/ParsePlatform/parse-server/issues/1198
|
||||
it('should cleanup null authData keys ParseUser update', (done) => {
|
||||
it('should cleanup null authData keys ParseUser update (regression test for #1198)', (done) => {
|
||||
Parse.Cloud.beforeSave('_User', (req, res) => {
|
||||
req.object.set('foo', 'bar');
|
||||
res.success();
|
||||
@@ -2273,4 +2287,124 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should not create extraneous session tokens', (done) => {
|
||||
let config = new Config(Parse.applicationId);
|
||||
config.database.loadSchema().then((s) => {
|
||||
// Lock down the _User class for creation
|
||||
return s.addClassIfNotExists('_User', {}, {create: {}})
|
||||
}).then((res) => {
|
||||
let user = new Parse.User();
|
||||
return user.save({'username': 'user', 'password': 'pass'});
|
||||
}).then(() => {
|
||||
fail('should not be able to save the user');
|
||||
}, (err) => {
|
||||
return Promise.resolve();
|
||||
}).then(() => {
|
||||
let q = new Parse.Query('_Session');
|
||||
return q.find({useMasterKey: true})
|
||||
}).then((res) => {
|
||||
// We should have no session created
|
||||
expect(res.length).toBe(0);
|
||||
done();
|
||||
}, (err) => {
|
||||
fail('should not fail');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not overwrite username when unlinking facebook user (regression test for #1532)', done => {
|
||||
Parse.Object.disableSingleInstance();
|
||||
var provider = getMockFacebookProvider();
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
var user = new Parse.User();
|
||||
user.set("username", "testLinkWithProvider");
|
||||
user.set("password", "mypass");
|
||||
user.signUp()
|
||||
.then(user => user._linkWith("facebook", {
|
||||
success: user => {
|
||||
expect(user.get('username')).toEqual('testLinkWithProvider');
|
||||
expect(Parse.FacebookUtils.isLinked(user)).toBeTruthy();
|
||||
return user._unlinkFrom('facebook')
|
||||
.then(() => user.fetch())
|
||||
.then(user => {
|
||||
expect(user.get('username')).toEqual('testLinkWithProvider');
|
||||
expect(Parse.FacebookUtils.isLinked(user)).toBeFalsy();
|
||||
done();
|
||||
});
|
||||
},
|
||||
error: e => {
|
||||
fail('Unexpected failure testing linking');
|
||||
fail(error);
|
||||
done();
|
||||
}
|
||||
}))
|
||||
.catch(error => {
|
||||
fail('Unexpected failure testing in unlink user test');
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should revoke sessions when converting anonymous user to "normal" user', done => {
|
||||
request.post({
|
||||
url: 'http://localhost:8378/1/classes/_User',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
json: {authData: {anonymous: {id: '00000000-0000-0000-0000-000000000001'}}}
|
||||
}, (err, res, body) => {
|
||||
Parse.User.become(body.sessionToken)
|
||||
.then(user => {
|
||||
let obj = new Parse.Object('TestObject');
|
||||
obj.setACL(new Parse.ACL(user));
|
||||
return obj.save()
|
||||
.then(() => {
|
||||
// Change password, revoking session
|
||||
user.set('username', 'no longer anonymous');
|
||||
user.set('password', 'password');
|
||||
return user.save()
|
||||
})
|
||||
.then(() => obj.fetch())
|
||||
.catch(error => {
|
||||
expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND);
|
||||
done();
|
||||
});
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
it('should not revoke session tokens if the server is configures to not revoke session tokens', done => {
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
masterKey: 'test',
|
||||
cloud: './spec/cloud/main.js',
|
||||
revokeSessionOnPasswordReset: false,
|
||||
})
|
||||
request.post({
|
||||
url: 'http://localhost:8378/1/classes/_User',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
json: {authData: {anonymous: {id: '00000000-0000-0000-0000-000000000001'}}}
|
||||
}, (err, res, body) => {
|
||||
Parse.User.become(body.sessionToken)
|
||||
.then(user => {
|
||||
let obj = new Parse.Object('TestObject');
|
||||
obj.setACL(new Parse.ACL(user));
|
||||
return obj.save()
|
||||
.then(() => {
|
||||
// Change password, revoking session
|
||||
user.set('username', 'no longer anonymous');
|
||||
user.set('password', 'password');
|
||||
return user.save()
|
||||
})
|
||||
.then(() => obj.fetch())
|
||||
// fetch should succeed as we still have our session token
|
||||
.then(done, fail);
|
||||
})
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
@@ -0,0 +1,697 @@
|
||||
'use strict';
|
||||
var Schema = require('../src/Controllers/SchemaController');
|
||||
|
||||
var Config = require('../src/Config');
|
||||
|
||||
describe('Pointer Permissions', () => {
|
||||
it('should work with find', (done) => {
|
||||
let config = new Config(Parse.applicationId);
|
||||
let user = new Parse.User();
|
||||
let user2 = new Parse.User();
|
||||
user.set({
|
||||
username: 'user1',
|
||||
password: 'password'
|
||||
});
|
||||
user2.set({
|
||||
username: 'user2',
|
||||
password: 'password'
|
||||
});
|
||||
let obj = new Parse.Object('AnObject');
|
||||
let obj2 = new Parse.Object('AnObject');
|
||||
|
||||
Parse.Object.saveAll([user, user2]).then(() => {
|
||||
obj.set('owner', user);
|
||||
obj2.set('owner', user2);
|
||||
return Parse.Object.saveAll([obj, obj2]);
|
||||
}).then(() => {
|
||||
return config.database.loadSchema().then((schema) => {
|
||||
return schema.updateClass('AnObject', {}, {readUserFields: ['owner']})
|
||||
});
|
||||
}).then(() => {
|
||||
return Parse.User.logIn('user1', 'password');
|
||||
}).then(() => {
|
||||
let q = new Parse.Query('AnObject');
|
||||
return q.find();
|
||||
}).then((res) => {
|
||||
expect(res.length).toBe(1);
|
||||
expect(res[0].id).toBe(obj.id);
|
||||
done();
|
||||
}).catch((err) => {
|
||||
fail('Should not fail');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should work with write', (done) => {
|
||||
let config = new Config(Parse.applicationId);
|
||||
let user = new Parse.User();
|
||||
let user2 = new Parse.User();
|
||||
user.set({
|
||||
username: 'user1',
|
||||
password: 'password'
|
||||
});
|
||||
user2.set({
|
||||
username: 'user2',
|
||||
password: 'password'
|
||||
});
|
||||
let obj = new Parse.Object('AnObject');
|
||||
let obj2 = new Parse.Object('AnObject');
|
||||
|
||||
Parse.Object.saveAll([user, user2]).then(() => {
|
||||
obj.set('owner', user);
|
||||
obj.set('reader', user2);
|
||||
obj2.set('owner', user2);
|
||||
obj2.set('reader', user);
|
||||
return Parse.Object.saveAll([obj, obj2]);
|
||||
}).then(() => {
|
||||
return config.database.loadSchema().then((schema) => {
|
||||
return schema.updateClass('AnObject', {}, {writeUserFields: ['owner'], readUserFields: ['reader', 'owner']});
|
||||
});
|
||||
}).then(() => {
|
||||
return Parse.User.logIn('user1', 'password');
|
||||
}).then(() => {
|
||||
obj2.set('hello', 'world');
|
||||
return obj2.save();
|
||||
}).then((res) => {
|
||||
fail('User should not be able to update obj2');
|
||||
}, (err) => {
|
||||
// User 1 should not be able to update obj2
|
||||
expect(err.code).toBe(101);
|
||||
return Promise.resolve();
|
||||
}).then(()=> {
|
||||
obj.set('hello', 'world');
|
||||
return obj.save();
|
||||
}).then(() => {
|
||||
return Parse.User.logIn('user2', 'password');
|
||||
}, (err) => {
|
||||
fail('User should be able to update');
|
||||
return Promise.resolve();
|
||||
}).then(() => {
|
||||
let q = new Parse.Query('AnObject');
|
||||
return q.find();
|
||||
}, (err) => {
|
||||
fail('should login with user 2');
|
||||
}).then((res) => {
|
||||
expect(res.length).toBe(2);
|
||||
res.forEach((result) => {
|
||||
if (result.id == obj.id) {
|
||||
expect(result.get('hello')).toBe('world');
|
||||
} else {
|
||||
expect(result.id).toBe(obj2.id);
|
||||
}
|
||||
})
|
||||
done();
|
||||
}, (err) => {
|
||||
fail("failed");
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('should let a proper user find', (done) => {
|
||||
let config = new Config(Parse.applicationId);
|
||||
let user = new Parse.User();
|
||||
let user2 = new Parse.User();
|
||||
user.set({
|
||||
username: 'user1',
|
||||
password: 'password'
|
||||
});
|
||||
user2.set({
|
||||
username: 'user2',
|
||||
password: 'password'
|
||||
});
|
||||
let obj = new Parse.Object('AnObject');
|
||||
let obj2 = new Parse.Object('AnObject');
|
||||
user.signUp().then(() => {
|
||||
return user2.signUp()
|
||||
}).then(() => {
|
||||
Parse.User.logOut();
|
||||
}).then(() => {
|
||||
obj.set('owner', user);
|
||||
return Parse.Object.saveAll([obj, obj2]);
|
||||
}).then(() => {
|
||||
return config.database.loadSchema().then((schema) => {
|
||||
return schema.updateClass('AnObject', {}, {find: {}, get:{}, readUserFields: ['owner']})
|
||||
});
|
||||
}).then(() => {
|
||||
let q = new Parse.Query('AnObject');
|
||||
return q.find();
|
||||
}).then((res) => {
|
||||
expect(res.length).toBe(0);
|
||||
}).then(() => {
|
||||
return Parse.User.logIn('user2', 'password');
|
||||
}).then(() => {
|
||||
let q = new Parse.Query('AnObject');
|
||||
return q.find();
|
||||
}).then((res) => {
|
||||
expect(res.length).toBe(0);
|
||||
let q = new Parse.Query('AnObject');
|
||||
return q.get(obj.id);
|
||||
}).then(() => {
|
||||
fail('User 2 should not get the obj1 object');
|
||||
}, (err) => {
|
||||
expect(err.code).toBe(101);
|
||||
expect(err.message).toBe('Object not found.');
|
||||
return Promise.resolve();
|
||||
}).then(() => {
|
||||
return Parse.User.logIn('user1', 'password');
|
||||
}).then(() => {
|
||||
let q = new Parse.Query('AnObject');
|
||||
return q.find();
|
||||
}).then((res) => {
|
||||
expect(res.length).toBe(1);
|
||||
done();
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
fail('should not fail');
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('should not allow creating objects', (done) => {
|
||||
let config = new Config(Parse.applicationId);
|
||||
let user = new Parse.User();
|
||||
user.set({
|
||||
username: 'user1',
|
||||
password: 'password'
|
||||
});
|
||||
let obj = new Parse.Object('AnObject');
|
||||
user.save().then(() => {
|
||||
return config.database.loadSchema().then((schema) => {
|
||||
return schema.addClassIfNotExists('AnObject', {owner: {type:'Pointer', targetClass: '_User'}}, {create: {}, writeUserFields: ['owner'], readUserFields: ['owner']});
|
||||
});
|
||||
}).then(() => {
|
||||
return Parse.User.logIn('user1', 'password');
|
||||
}).then(() => {
|
||||
obj.set('owner', user);
|
||||
return obj.save();
|
||||
}).then(() => {
|
||||
fail('should not succeed');
|
||||
done();
|
||||
}, (err) => {
|
||||
expect(err.code).toBe(119);
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('should handle multiple writeUserFields', (done) => {
|
||||
let config = new Config(Parse.applicationId);
|
||||
let user = new Parse.User();
|
||||
let user2 = new Parse.User();
|
||||
user.set({
|
||||
username: 'user1',
|
||||
password: 'password'
|
||||
});
|
||||
user2.set({
|
||||
username: 'user2',
|
||||
password: 'password'
|
||||
});
|
||||
let obj = new Parse.Object('AnObject');
|
||||
Parse.Object.saveAll([user, user2]).then(() => {
|
||||
obj.set('owner', user);
|
||||
obj.set('otherOwner', user2);
|
||||
return obj.save();
|
||||
}).then(() => {
|
||||
return config.database.loadSchema().then((schema) => {
|
||||
return schema.updateClass('AnObject', {}, {find: {"*": true},writeUserFields: ['owner', 'otherOwner']});
|
||||
});
|
||||
}).then(() => {
|
||||
return Parse.User.logIn('user1', 'password');
|
||||
}).then(() => {
|
||||
return obj.save({hello: 'fromUser1'});
|
||||
}).then(() => {
|
||||
return Parse.User.logIn('user2', 'password');
|
||||
}).then(() => {
|
||||
return obj.save({hello: 'fromUser2'});
|
||||
}).then(() => {
|
||||
Parse.User.logOut();
|
||||
let q = new Parse.Query('AnObject');
|
||||
return q.first();
|
||||
}).then((result) => {
|
||||
expect(result.get('hello')).toBe('fromUser2');
|
||||
done();
|
||||
}).catch(err => {
|
||||
fail('should not fail');
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('should prevent creating pointer permission on missing field', (done) => {
|
||||
let config = new Config(Parse.applicationId);
|
||||
config.database.loadSchema().then((schema) => {
|
||||
return schema.addClassIfNotExists('AnObject', {}, {create: {}, writeUserFields: ['owner'], readUserFields: ['owner']});
|
||||
}).then(() => {
|
||||
fail('should not succeed');
|
||||
}).catch((err) => {
|
||||
expect(err.code).toBe(107);
|
||||
expect(err.message).toBe("'owner' is not a valid column for class level pointer permissions writeUserFields");
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('should prevent creating pointer permission on bad field', (done) => {
|
||||
let config = new Config(Parse.applicationId);
|
||||
config.database.loadSchema().then((schema) => {
|
||||
return schema.addClassIfNotExists('AnObject', {owner: {type: 'String'}}, {create: {}, writeUserFields: ['owner'], readUserFields: ['owner']});
|
||||
}).then(() => {
|
||||
fail('should not succeed');
|
||||
}).catch((err) => {
|
||||
expect(err.code).toBe(107);
|
||||
expect(err.message).toBe("'owner' is not a valid column for class level pointer permissions writeUserFields");
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('should prevent creating pointer permission on bad field', (done) => {
|
||||
let config = new Config(Parse.applicationId);
|
||||
let object = new Parse.Object('AnObject');
|
||||
object.set('owner', 'value');
|
||||
object.save().then(() => {
|
||||
return config.database.loadSchema();
|
||||
}).then((schema) => {
|
||||
return schema.updateClass('AnObject', {}, {create: {}, writeUserFields: ['owner'], readUserFields: ['owner']});
|
||||
}).then(() => {
|
||||
fail('should not succeed');
|
||||
}).catch((err) => {
|
||||
expect(err.code).toBe(107);
|
||||
expect(err.message).toBe("'owner' is not a valid column for class level pointer permissions writeUserFields");
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('tests CLP / Pointer Perms / ACL write (PP Locked)', (done) => {
|
||||
/*
|
||||
tests:
|
||||
CLP: update open ({"*": true})
|
||||
PointerPerm: "owner"
|
||||
ACL: logged in user has access
|
||||
|
||||
The owner is another user than the ACL
|
||||
*/
|
||||
let config = new Config(Parse.applicationId);
|
||||
let user = new Parse.User();
|
||||
let user2 = new Parse.User();
|
||||
user.set({
|
||||
username: 'user1',
|
||||
password: 'password'
|
||||
});
|
||||
user2.set({
|
||||
username: 'user2',
|
||||
password: 'password'
|
||||
});
|
||||
let obj = new Parse.Object('AnObject');
|
||||
Parse.Object.saveAll([user, user2]).then(() => {
|
||||
let ACL = new Parse.ACL();
|
||||
ACL.setReadAccess(user, true);
|
||||
ACL.setWriteAccess(user, true);
|
||||
obj.setACL(ACL);
|
||||
obj.set('owner', user2);
|
||||
return obj.save();
|
||||
}).then(() => {
|
||||
return config.database.loadSchema().then((schema) => {
|
||||
// Lock the update, and let only owner write
|
||||
return schema.updateClass('AnObject', {}, {update: {"*": true}, writeUserFields: ['owner']});
|
||||
});
|
||||
}).then(() => {
|
||||
return Parse.User.logIn('user1', 'password');
|
||||
}).then(() => {
|
||||
// user1 has ACL read/write but should be blocked by PP
|
||||
return obj.save({key: 'value'});
|
||||
}).then(() => {
|
||||
fail('Should not succeed saving');
|
||||
done();
|
||||
}, (err) => {
|
||||
expect(err.code).toBe(101);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('tests CLP / Pointer Perms / ACL write (ACL Locked)', (done) => {
|
||||
/*
|
||||
tests:
|
||||
CLP: update open ({"*": true})
|
||||
PointerPerm: "owner"
|
||||
ACL: logged in user has access
|
||||
*/
|
||||
let config = new Config(Parse.applicationId);
|
||||
let user = new Parse.User();
|
||||
let user2 = new Parse.User();
|
||||
user.set({
|
||||
username: 'user1',
|
||||
password: 'password'
|
||||
});
|
||||
user2.set({
|
||||
username: 'user2',
|
||||
password: 'password'
|
||||
});
|
||||
let obj = new Parse.Object('AnObject');
|
||||
Parse.Object.saveAll([user, user2]).then(() => {
|
||||
let ACL = new Parse.ACL();
|
||||
ACL.setReadAccess(user, true);
|
||||
ACL.setWriteAccess(user, true);
|
||||
obj.setACL(ACL);
|
||||
obj.set('owner', user2);
|
||||
return obj.save();
|
||||
}).then(() => {
|
||||
return config.database.loadSchema().then((schema) => {
|
||||
// Lock the update, and let only owner write
|
||||
return schema.updateClass('AnObject', {}, {update: {"*": true}, writeUserFields: ['owner']});
|
||||
});
|
||||
}).then(() => {
|
||||
return Parse.User.logIn('user2', 'password');
|
||||
}).then(() => {
|
||||
// user1 has ACL read/write but should be blocked by ACL
|
||||
return obj.save({key: 'value'});
|
||||
}).then(() => {
|
||||
fail('Should not succeed saving');
|
||||
done();
|
||||
}, (err) => {
|
||||
expect(err.code).toBe(101);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('tests CLP / Pointer Perms / ACL write (ACL/PP OK)', (done) => {
|
||||
/*
|
||||
tests:
|
||||
CLP: update open ({"*": true})
|
||||
PointerPerm: "owner"
|
||||
ACL: logged in user has access
|
||||
*/
|
||||
let config = new Config(Parse.applicationId);
|
||||
let user = new Parse.User();
|
||||
let user2 = new Parse.User();
|
||||
user.set({
|
||||
username: 'user1',
|
||||
password: 'password'
|
||||
});
|
||||
user2.set({
|
||||
username: 'user2',
|
||||
password: 'password'
|
||||
});
|
||||
let obj = new Parse.Object('AnObject');
|
||||
Parse.Object.saveAll([user, user2]).then(() => {
|
||||
let ACL = new Parse.ACL();
|
||||
ACL.setWriteAccess(user, true);
|
||||
ACL.setWriteAccess(user2, true);
|
||||
obj.setACL(ACL);
|
||||
obj.set('owner', user2);
|
||||
return obj.save();
|
||||
}).then(() => {
|
||||
return config.database.loadSchema().then((schema) => {
|
||||
// Lock the update, and let only owner write
|
||||
return schema.updateClass('AnObject', {}, {update: {"*": true}, writeUserFields: ['owner']});
|
||||
});
|
||||
}).then(() => {
|
||||
return Parse.User.logIn('user2', 'password');
|
||||
}).then(() => {
|
||||
// user1 has ACL read/write but should be blocked by ACL
|
||||
return obj.save({key: 'value'});
|
||||
}).then((objAgain) => {
|
||||
expect(objAgain.get('key')).toBe('value');
|
||||
done();
|
||||
}, (err) => {
|
||||
fail('Should not fail saving');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('tests CLP / Pointer Perms / ACL read (PP locked)', (done) => {
|
||||
/*
|
||||
tests:
|
||||
CLP: find/get open ({"*": true})
|
||||
PointerPerm: "owner" : read
|
||||
ACL: logged in user has access
|
||||
|
||||
The owner is another user than the ACL
|
||||
*/
|
||||
let config = new Config(Parse.applicationId);
|
||||
let user = new Parse.User();
|
||||
let user2 = new Parse.User();
|
||||
user.set({
|
||||
username: 'user1',
|
||||
password: 'password'
|
||||
});
|
||||
user2.set({
|
||||
username: 'user2',
|
||||
password: 'password'
|
||||
});
|
||||
let obj = new Parse.Object('AnObject');
|
||||
Parse.Object.saveAll([user, user2]).then(() => {
|
||||
let ACL = new Parse.ACL();
|
||||
ACL.setReadAccess(user, true);
|
||||
ACL.setWriteAccess(user, true);
|
||||
obj.setACL(ACL);
|
||||
obj.set('owner', user2);
|
||||
return obj.save();
|
||||
}).then(() => {
|
||||
return config.database.loadSchema().then((schema) => {
|
||||
// Lock the update, and let only owner write
|
||||
return schema.updateClass('AnObject', {}, {find: {"*": true}, get: {"*": true}, readUserFields: ['owner']});
|
||||
});
|
||||
}).then(() => {
|
||||
return Parse.User.logIn('user1', 'password');
|
||||
}).then(() => {
|
||||
// user1 has ACL read/write but should be block
|
||||
return obj.fetch();
|
||||
}).then(() => {
|
||||
fail('Should not succeed saving');
|
||||
done();
|
||||
}, (err) => {
|
||||
expect(err.code).toBe(101);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('tests CLP / Pointer Perms / ACL read (PP/ACL OK)', (done) => {
|
||||
/*
|
||||
tests:
|
||||
CLP: find/get open ({"*": true})
|
||||
PointerPerm: "owner" : read
|
||||
ACL: logged in user has access
|
||||
*/
|
||||
let config = new Config(Parse.applicationId);
|
||||
let user = new Parse.User();
|
||||
let user2 = new Parse.User();
|
||||
user.set({
|
||||
username: 'user1',
|
||||
password: 'password'
|
||||
});
|
||||
user2.set({
|
||||
username: 'user2',
|
||||
password: 'password'
|
||||
});
|
||||
let obj = new Parse.Object('AnObject');
|
||||
Parse.Object.saveAll([user, user2]).then(() => {
|
||||
let ACL = new Parse.ACL();
|
||||
ACL.setReadAccess(user, true);
|
||||
ACL.setWriteAccess(user, true);
|
||||
ACL.setReadAccess(user2, true);
|
||||
ACL.setWriteAccess(user2, true);
|
||||
obj.setACL(ACL);
|
||||
obj.set('owner', user2);
|
||||
return obj.save();
|
||||
}).then(() => {
|
||||
return config.database.loadSchema().then((schema) => {
|
||||
// Lock the update, and let only owner write
|
||||
return schema.updateClass('AnObject', {}, {find: {"*": true}, get: {"*": true}, readUserFields: ['owner']});
|
||||
});
|
||||
}).then(() => {
|
||||
return Parse.User.logIn('user2', 'password');
|
||||
}).then(() => {
|
||||
// user1 has ACL read/write but should be block
|
||||
return obj.fetch();
|
||||
}).then((objAgain) => {
|
||||
expect(objAgain.id).toBe(obj.id);
|
||||
done();
|
||||
}, (err) => {
|
||||
fail('Should not fail fetching');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('tests CLP / Pointer Perms / ACL read (ACL locked)', (done) => {
|
||||
/*
|
||||
tests:
|
||||
CLP: find/get open ({"*": true})
|
||||
PointerPerm: "owner" : read // proper owner
|
||||
ACL: logged in user has not access
|
||||
*/
|
||||
let config = new Config(Parse.applicationId);
|
||||
let user = new Parse.User();
|
||||
let user2 = new Parse.User();
|
||||
user.set({
|
||||
username: 'user1',
|
||||
password: 'password'
|
||||
});
|
||||
user2.set({
|
||||
username: 'user2',
|
||||
password: 'password'
|
||||
});
|
||||
let obj = new Parse.Object('AnObject');
|
||||
Parse.Object.saveAll([user, user2]).then(() => {
|
||||
let ACL = new Parse.ACL();
|
||||
ACL.setReadAccess(user, true);
|
||||
ACL.setWriteAccess(user, true);
|
||||
obj.setACL(ACL);
|
||||
obj.set('owner', user2);
|
||||
return obj.save();
|
||||
}).then(() => {
|
||||
return config.database.loadSchema().then((schema) => {
|
||||
// Lock the update, and let only owner write
|
||||
return schema.updateClass('AnObject', {}, {find: {"*": true}, get: {"*": true}, readUserFields: ['owner']});
|
||||
});
|
||||
}).then(() => {
|
||||
return Parse.User.logIn('user2', 'password');
|
||||
}).then(() => {
|
||||
// user2 has ACL read/write but should be block by ACL
|
||||
return obj.fetch();
|
||||
}).then(() => {
|
||||
fail('Should not succeed saving');
|
||||
done();
|
||||
}, (err) => {
|
||||
expect(err.code).toBe(101);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should let master key find objects', (done) => {
|
||||
let config = new Config(Parse.applicationId);
|
||||
let user = new Parse.User();
|
||||
let object = new Parse.Object('AnObject');
|
||||
object.set('hello', 'world');
|
||||
return object.save().then(() => {
|
||||
return config.database.loadSchema().then((schema) => {
|
||||
// Lock the update, and let only owner write
|
||||
return schema.updateClass('AnObject', {owner: {type: 'Pointer', targetClass: '_User'}}, {find: {}, get: {}, readUserFields: ['owner']});
|
||||
});
|
||||
}).then(() => {
|
||||
let q = new Parse.Query('AnObject');
|
||||
return q.find();
|
||||
}).then(() => {
|
||||
|
||||
}, (err) => {
|
||||
expect(err.code).toBe(101);
|
||||
return Promise.resolve();
|
||||
}).then(() => {
|
||||
let q = new Parse.Query('AnObject');
|
||||
return q.find({useMasterKey: true});
|
||||
}).then((objects) => {
|
||||
expect(objects.length).toBe(1);
|
||||
done();
|
||||
}, (err) => {
|
||||
fail('master key should find the object');
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('should let master key get objects', (done) => {
|
||||
let config = new Config(Parse.applicationId);
|
||||
let user = new Parse.User();
|
||||
let object = new Parse.Object('AnObject');
|
||||
object.set('hello', 'world');
|
||||
return object.save().then(() => {
|
||||
return config.database.loadSchema().then((schema) => {
|
||||
// Lock the update, and let only owner write
|
||||
return schema.updateClass('AnObject', {owner: {type: 'Pointer', targetClass: '_User'}}, {find: {}, get: {}, readUserFields: ['owner']});
|
||||
});
|
||||
}).then(() => {
|
||||
let q = new Parse.Query('AnObject');
|
||||
return q.get(object.id);
|
||||
}).then(() => {
|
||||
|
||||
}, (err) => {
|
||||
expect(err.code).toBe(101);
|
||||
return Promise.resolve();
|
||||
}).then(() => {
|
||||
let q = new Parse.Query('AnObject');
|
||||
return q.get(object.id, {useMasterKey: true});
|
||||
}).then((objectAgain) => {
|
||||
expect(objectAgain).not.toBeUndefined();
|
||||
expect(objectAgain.id).toBe(object.id);
|
||||
done();
|
||||
}, (err) => {
|
||||
fail('master key should find the object');
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
it('should let master key update objects', (done) => {
|
||||
let config = new Config(Parse.applicationId);
|
||||
let user = new Parse.User();
|
||||
let object = new Parse.Object('AnObject');
|
||||
object.set('hello', 'world');
|
||||
return object.save().then(() => {
|
||||
return config.database.loadSchema().then((schema) => {
|
||||
// Lock the update, and let only owner write
|
||||
return schema.updateClass('AnObject', {owner: {type: 'Pointer', targetClass: '_User'}}, {update: {}, writeUserFields: ['owner']});
|
||||
});
|
||||
}).then(() => {
|
||||
return object.save({'hello': 'bar'});
|
||||
}).then(() => {
|
||||
|
||||
}, (err) => {
|
||||
expect(err.code).toBe(101);
|
||||
return Promise.resolve();
|
||||
}).then(() => {
|
||||
return object.save({'hello': 'baz'}, {useMasterKey: true});
|
||||
}).then((objectAgain) => {
|
||||
expect(objectAgain.get('hello')).toBe('baz');
|
||||
done();
|
||||
}, (err) => {
|
||||
fail('master key should save the object');
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('should let master key delete objects', (done) => {
|
||||
let config = new Config(Parse.applicationId);
|
||||
let user = new Parse.User();
|
||||
let object = new Parse.Object('AnObject');
|
||||
object.set('hello', 'world');
|
||||
return object.save().then(() => {
|
||||
return config.database.loadSchema().then((schema) => {
|
||||
// Lock the update, and let only owner write
|
||||
return schema.updateClass('AnObject', {owner: {type: 'Pointer', targetClass: '_User'}}, {delete: {}, writeUserFields: ['owner']});
|
||||
});
|
||||
}).then(() => {
|
||||
return object.destroy();
|
||||
}).then(() => {
|
||||
fail();
|
||||
}, (err) => {
|
||||
expect(err.code).toBe(101);
|
||||
return Promise.resolve();
|
||||
}).then(() => {
|
||||
return object.destroy({useMasterKey: true});
|
||||
}).then((objectAgain) => {
|
||||
done();
|
||||
}, (err) => {
|
||||
fail('master key should destroy the object');
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('should fail with invalid pointer perms', () => {
|
||||
let config = new Config(Parse.applicationId);
|
||||
config.database.loadSchema().then((schema) => {
|
||||
// Lock the update, and let only owner write
|
||||
return schema.addClassIfNotExists('AnObject', {owner: {type: 'Pointer', targetClass: '_User'}}, {delete: {}, writeUserFields: 'owner'});
|
||||
}).catch((err) => {
|
||||
expect(err.code).toBe(Parse.Error.INVALID_JSON);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail with invalid pointer perms', () => {
|
||||
let config = new Config(Parse.applicationId);
|
||||
config.database.loadSchema().then((schema) => {
|
||||
// Lock the update, and let only owner write
|
||||
return schema.addClassIfNotExists('AnObject', {owner: {type: 'Pointer', targetClass: '_User'}}, {delete: {}, writeUserFields: ['owner', 'invalid']});
|
||||
}).catch((err) => {
|
||||
expect(err.code).toBe(Parse.Error.INVALID_JSON);
|
||||
done();
|
||||
});
|
||||
})
|
||||
|
||||
});
|
||||
@@ -293,6 +293,7 @@ describe('PushController', () => {
|
||||
expect(results.length).toBe(1);
|
||||
let result = results[0];
|
||||
expect(result.createdAt instanceof Date).toBe(true);
|
||||
expect(result.updatedAt instanceof Date).toBe(true);
|
||||
expect(result.id.length).toBe(10);
|
||||
expect(result.get('source')).toEqual('rest');
|
||||
expect(result.get('query')).toEqual(JSON.stringify({}));
|
||||
|
||||
@@ -352,4 +352,31 @@ describe('rest create', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("can create a session with no expiration", (done) => {
|
||||
var user = {
|
||||
username: 'asdf',
|
||||
password: 'zxcv',
|
||||
foo: 'bar'
|
||||
};
|
||||
config.expireInactiveSessions = false;
|
||||
|
||||
rest.create(config, auth.nobody(config), '_User', user)
|
||||
.then((r) => {
|
||||
expect(Object.keys(r.response).length).toEqual(3);
|
||||
expect(typeof r.response.objectId).toEqual('string');
|
||||
expect(typeof r.response.createdAt).toEqual('string');
|
||||
expect(typeof r.response.sessionToken).toEqual('string');
|
||||
return rest.find(config, auth.master(config),
|
||||
'_Session', {sessionToken: r.response.sessionToken});
|
||||
})
|
||||
.then((r) => {
|
||||
expect(r.results.length).toEqual(1);
|
||||
|
||||
var session = r.results[0];
|
||||
expect(session.expiresAt).toBeUndefined();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -167,7 +167,6 @@ describe('rest query', () => {
|
||||
expect(error).toBe(null);
|
||||
var b = JSON.parse(body);
|
||||
expect(b.code).toEqual(Parse.Error.INVALID_QUERY);
|
||||
expect(b.error).toEqual('Improper encode of parameter');
|
||||
done();
|
||||
});
|
||||
}).then(() => {
|
||||
@@ -185,7 +184,6 @@ describe('rest query', () => {
|
||||
expect(error).toBe(null);
|
||||
var b = JSON.parse(body);
|
||||
expect(b.code).toEqual(Parse.Error.INVALID_QUERY);
|
||||
expect(b.error).toEqual('Improper encode of parameter');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
+34
-35
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
var Config = require('../src/Config');
|
||||
var Schema = require('../src/Schema');
|
||||
var SchemaController = require('../src/Controllers/SchemaController');
|
||||
var dd = require('deep-diff');
|
||||
|
||||
var config = new Config('test');
|
||||
@@ -19,7 +19,7 @@ var hasAllPODobject = () => {
|
||||
return obj;
|
||||
};
|
||||
|
||||
describe('Schema', () => {
|
||||
describe('SchemaController', () => {
|
||||
it('can validate one object', (done) => {
|
||||
config.database.loadSchema().then((schema) => {
|
||||
return schema.validateObject('TestObject', {a: 1, b: 'yo', c: false});
|
||||
@@ -121,39 +121,38 @@ describe('Schema', () => {
|
||||
});
|
||||
|
||||
it('class-level permissions test get', (done) => {
|
||||
var user;
|
||||
var obj;
|
||||
createTestUser().then((u) => {
|
||||
user = u;
|
||||
return config.database.loadSchema();
|
||||
}).then((schema) => {
|
||||
// Just to create a valid class
|
||||
return schema.validateObject('Stuff', {foo: 'bar'});
|
||||
}).then((schema) => {
|
||||
var find = {};
|
||||
var get = {};
|
||||
get[user.id] = true;
|
||||
return schema.setPermissions('Stuff', {
|
||||
'find': find,
|
||||
'get': get
|
||||
});
|
||||
}).then((schema) => {
|
||||
obj = new Parse.Object('Stuff');
|
||||
obj.set('foo', 'bar');
|
||||
return obj.save();
|
||||
}).then((o) => {
|
||||
obj = o;
|
||||
var query = new Parse.Query('Stuff');
|
||||
return query.find();
|
||||
}).then((results) => {
|
||||
fail('Class permissions should have rejected this query.');
|
||||
done();
|
||||
}, (e) => {
|
||||
var query = new Parse.Query('Stuff');
|
||||
return query.get(obj.id).then((o) => {
|
||||
createTestUser()
|
||||
.then(user => {
|
||||
return config.database.loadSchema()
|
||||
// Create a valid class
|
||||
.then(schema => schema.validateObject('Stuff', {foo: 'bar'}))
|
||||
.then(schema => {
|
||||
var find = {};
|
||||
var get = {};
|
||||
get[user.id] = true;
|
||||
return schema.setPermissions('Stuff', {
|
||||
'find': find,
|
||||
'get': get
|
||||
});
|
||||
}).then((schema) => {
|
||||
obj = new Parse.Object('Stuff');
|
||||
obj.set('foo', 'bar');
|
||||
return obj.save();
|
||||
}).then((o) => {
|
||||
obj = o;
|
||||
var query = new Parse.Query('Stuff');
|
||||
return query.find();
|
||||
}).then((results) => {
|
||||
fail('Class permissions should have rejected this query.');
|
||||
done();
|
||||
}, (e) => {
|
||||
fail('Class permissions should have allowed this get query');
|
||||
var query = new Parse.Query('Stuff');
|
||||
return query.get(obj.id).then((o) => {
|
||||
done();
|
||||
}, (e) => {
|
||||
fail('Class permissions should have allowed this get query');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -751,7 +750,7 @@ describe('Schema', () => {
|
||||
});
|
||||
|
||||
it('can merge schemas', done => {
|
||||
expect(Schema.buildMergedSchemaObject({
|
||||
expect(SchemaController.buildMergedSchemaObject({
|
||||
_id: 'SomeClass',
|
||||
someType: { type: 'Number' }
|
||||
}, {
|
||||
@@ -764,7 +763,7 @@ describe('Schema', () => {
|
||||
});
|
||||
|
||||
it('can merge deletions', done => {
|
||||
expect(Schema.buildMergedSchemaObject({
|
||||
expect(SchemaController.buildMergedSchemaObject({
|
||||
_id: 'SomeClass',
|
||||
someType: { type: 'Number' },
|
||||
outDatedType: { type: 'String' },
|
||||
@@ -779,7 +778,7 @@ describe('Schema', () => {
|
||||
});
|
||||
|
||||
it('ignore default field when merge with system class', done => {
|
||||
expect(Schema.buildMergedSchemaObject({
|
||||
expect(SchemaController.buildMergedSchemaObject({
|
||||
_id: '_User',
|
||||
username: { type: 'String' },
|
||||
password: { type: 'String' },
|
||||
|
||||
+26
-4
@@ -9,6 +9,7 @@ var facebook = require('../src/authDataManager/facebook');
|
||||
var ParseServer = require('../src/index').ParseServer;
|
||||
var path = require('path');
|
||||
var TestUtils = require('../src/index').TestUtils;
|
||||
var MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter');
|
||||
|
||||
var databaseURI = process.env.DATABASE_URI;
|
||||
var cloudMain = process.env.CLOUD_CODE_MAIN || './spec/cloud/main.js';
|
||||
@@ -40,7 +41,7 @@ var defaultConfiguration = {
|
||||
myoauth: {
|
||||
module: path.resolve(__dirname, "myoauth") // relative path as it's run from src
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Set up a default API server for testing with default configuration.
|
||||
@@ -54,7 +55,7 @@ delete defaultConfiguration.cloud;
|
||||
|
||||
var currentConfiguration;
|
||||
// Allows testing specific configurations of Parse Server
|
||||
var setServerConfiguration = configuration => {
|
||||
const setServerConfiguration = configuration => {
|
||||
// the configuration hasn't changed
|
||||
if (configuration === currentConfiguration) {
|
||||
return;
|
||||
@@ -84,11 +85,32 @@ beforeEach(function(done) {
|
||||
Parse.initialize('test', 'test', 'test');
|
||||
Parse.serverURL = 'http://localhost:' + port + '/1';
|
||||
Parse.User.enableUnsafeCurrentUser();
|
||||
done();
|
||||
return TestUtils.destroyAllDataPermanently().then(done, fail);
|
||||
});
|
||||
|
||||
var mongoAdapter = new MongoStorageAdapter({
|
||||
collectionPrefix: defaultConfiguration.collectionPrefix,
|
||||
uri: databaseURI,
|
||||
})
|
||||
|
||||
afterEach(function(done) {
|
||||
Parse.User.logOut().then(() => {
|
||||
mongoAdapter.getAllSchemas()
|
||||
.then(allSchemas => {
|
||||
allSchemas.forEach((schema) => {
|
||||
var className = schema.className;
|
||||
expect(className).toEqual({ asymmetricMatch: className => {
|
||||
if (!className.startsWith('_')) {
|
||||
return true;
|
||||
} else {
|
||||
// Other system classes will break Parse.com, so make sure that we don't save anything to _SCHEMA that will
|
||||
// break it.
|
||||
return ['_User', '_Installation', '_Role', '_Session', '_Product'].includes(className);
|
||||
}
|
||||
}});
|
||||
});
|
||||
})
|
||||
.then(() => Parse.User.logOut())
|
||||
.then(() => {
|
||||
return TestUtils.destroyAllDataPermanently();
|
||||
}).then(() => {
|
||||
done();
|
||||
|
||||
@@ -332,5 +332,33 @@ describe('server', () => {
|
||||
sessionLength: '0'
|
||||
})).toThrow('Session length must be a value greater than 0.');
|
||||
done();
|
||||
});
|
||||
|
||||
it('ignores the session length when expireInactiveSessions set to false', (done) => {
|
||||
expect(() => setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
masterKey: 'test',
|
||||
sessionLength: '-33',
|
||||
expireInactiveSessions: false
|
||||
})).not.toThrow();
|
||||
|
||||
expect(() => setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
masterKey: 'test',
|
||||
sessionLength: '0',
|
||||
expireInactiveSessions: false
|
||||
})).not.toThrow();
|
||||
done();
|
||||
})
|
||||
|
||||
it('fails if you try to set revokeSessionOnPasswordReset to non-boolean', done => {
|
||||
expect(() => setServerConfiguration({ revokeSessionOnPasswordReset: 'non-bool' })).toThrow();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
import MongoCollection from './MongoCollection';
|
||||
import * as transform from './MongoTransform';
|
||||
import * as transform from './MongoTransform';
|
||||
|
||||
function mongoFieldToParseSchemaField(type) {
|
||||
if (type[0] === '*') {
|
||||
@@ -128,18 +128,12 @@ class MongoSchemaCollection {
|
||||
this._collection = collection;
|
||||
}
|
||||
|
||||
// Return a promise for all schemas known to this adapter, in Parse format. In case the
|
||||
// schemas cannot be retrieved, returns a promise that rejects. Requirements fot the
|
||||
// rejection reason are TBD.
|
||||
getAllSchemas() {
|
||||
_fetchAllSchemasFrom_SCHEMA() {
|
||||
return this._collection._rawFind({})
|
||||
.then(schemas => schemas.map(mongoSchemaToParseSchema));
|
||||
}
|
||||
|
||||
// Return a promise for the schema with the given name, in Parse format. If
|
||||
// this adapter doesn't know about the schema, return a promise that rejects with
|
||||
// undefined as the reason.
|
||||
findSchema(name: string) {
|
||||
_fechOneSchemaFrom_SCHEMA(name: string) {
|
||||
return this._collection._rawFind(_mongoSchemaQueryFromNameQuery(name), { limit: 1 }).then(results => {
|
||||
if (results.length === 1) {
|
||||
return mongoSchemaToParseSchema(results[0]);
|
||||
@@ -185,21 +179,45 @@ class MongoSchemaCollection {
|
||||
return this._collection.upsertOne(_mongoSchemaQueryFromNameQuery(name, query), update);
|
||||
}
|
||||
|
||||
updateField(className: string, fieldName: string, type: string) {
|
||||
// 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!
|
||||
let query = {};
|
||||
query[fieldName] = { '$exists': false };
|
||||
let update = {};
|
||||
if (typeof type === 'string') {
|
||||
type = {
|
||||
type: type
|
||||
// Add a field to the schema. If database does not support the field
|
||||
// type (e.g. mongo doesn't support more than one GeoPoint in a class) reject with an "Incorrect Type"
|
||||
// Parse error with a desciptive message. If the field already exists, this function must
|
||||
// not modify the schema, and must reject with an error. Exact error format is TBD. If this function
|
||||
// is called for a class that doesn't exist, this function must create that class.
|
||||
|
||||
// TODO: throw an error if an unsupported field type is passed. Deciding whether a type is supported
|
||||
// should be the job of the adapter. Some adapters may not support GeoPoint at all. Others may
|
||||
// Support additional types that Mongo doesn't, like Money, or something.
|
||||
|
||||
// TODO: don't spend an extra query on finding the schema if the type we are trying to add isn't a GeoPoint.
|
||||
addFieldIfNotExists(className: string, fieldName: string, type: string) {
|
||||
return this._fechOneSchemaFrom_SCHEMA(className)
|
||||
.then(schema => {
|
||||
// The schema exists. Check for existing GeoPoints.
|
||||
if (type.type === 'GeoPoint') {
|
||||
// Make sure there are not other geopoint fields
|
||||
if (Object.keys(schema.fields).some(existingField => schema.fields[existingField].type === 'GeoPoint')) {
|
||||
return Promise.reject(new Parse.Error(Parse.Error.INCORRECT_TYPE, 'MongoDB only supports one GeoPoint field in a class.'));
|
||||
}
|
||||
}
|
||||
}
|
||||
update[fieldName] = parseFieldTypeToMongoFieldType(type);
|
||||
update = {'$set': update};
|
||||
return this.upsertSchema(className, query, update);
|
||||
return Promise.resolve();
|
||||
}, error => {
|
||||
// If error is undefined, the schema doesn't exist, and we can create the schema with the field.
|
||||
// If some other error, reject with it.
|
||||
if (error === undefined) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
throw Promise.reject(error);
|
||||
})
|
||||
.then(() => {
|
||||
// We use $exists and $set to avoid overwriting the field type if it
|
||||
// already exists. (it could have added inbetween the last query and the update)
|
||||
return this.upsertSchema(
|
||||
className,
|
||||
{ [fieldName]: { '$exists': false } },
|
||||
{ '$set' : { [fieldName]: parseFieldTypeToMongoFieldType(type) } }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
get transform() {
|
||||
|
||||
@@ -10,6 +10,21 @@ let MongoClient = mongodb.MongoClient;
|
||||
const MongoSchemaCollectionName = '_SCHEMA';
|
||||
const DefaultMongoURI = 'mongodb://localhost:27017/parse';
|
||||
|
||||
const storageAdapterAllCollections = mongoAdapter => {
|
||||
return mongoAdapter.connect()
|
||||
.then(() => mongoAdapter.database.collections())
|
||||
.then(collections => {
|
||||
return collections.filter(collection => {
|
||||
if (collection.namespace.match(/\.system\./)) {
|
||||
return false;
|
||||
}
|
||||
// TODO: If you have one app with a collection prefix that happens to be a prefix of another
|
||||
// apps prefix, this will go very very badly. We should fix that somehow.
|
||||
return (collection.collectionName.indexOf(mongoAdapter._collectionPrefix) == 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export class MongoStorageAdapter {
|
||||
// Private
|
||||
_uri: string;
|
||||
@@ -70,22 +85,24 @@ export class MongoStorageAdapter {
|
||||
});
|
||||
}
|
||||
|
||||
dropCollection(name: string) {
|
||||
return this.collection(this._collectionPrefix + name).then(collection => collection.drop());
|
||||
// Deletes a schema. Resolve if successful. If the schema doesn't
|
||||
// exist, resolve with undefined. If schema exists, but can't be deleted for some other reason,
|
||||
// reject with INTERNAL_SERVER_ERROR.
|
||||
deleteOneSchema(className: string) {
|
||||
return this.collection(this._collectionPrefix + className).then(collection => collection.drop())
|
||||
.catch(error => {
|
||||
// 'ns not found' means collection was already gone. Ignore deletion attempt.
|
||||
if (error.message == 'ns not found') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(error);
|
||||
});
|
||||
}
|
||||
|
||||
// Used for testing only right now.
|
||||
allCollections() {
|
||||
return this.connect().then(() => {
|
||||
return this.database.collections();
|
||||
}).then(collections => {
|
||||
return collections.filter(collection => {
|
||||
if (collection.namespace.match(/\.system\./)) {
|
||||
return false;
|
||||
}
|
||||
return (collection.collectionName.indexOf(this._collectionPrefix) == 0);
|
||||
});
|
||||
});
|
||||
// Delete all data known to this adatper. Used for testing.
|
||||
deleteAllSchemas() {
|
||||
return storageAdapterAllCollections(this)
|
||||
.then(collections => Promise.all(collections.map(collection => collection.drop())));
|
||||
}
|
||||
|
||||
// Remove the column and all the data. For Relations, the _Join collection is handled
|
||||
@@ -127,6 +144,52 @@ export class MongoStorageAdapter {
|
||||
.then(schemaCollection => schemaCollection.updateSchema(className, schemaUpdate));
|
||||
}
|
||||
|
||||
// Return a promise for all schemas known to this adapter, in Parse format. In case the
|
||||
// schemas cannot be retrieved, returns a promise that rejects. Requirements for the
|
||||
// rejection reason are TBD.
|
||||
getAllSchemas() {
|
||||
return this.schemaCollection().then(schemasCollection => schemasCollection._fetchAllSchemasFrom_SCHEMA());
|
||||
}
|
||||
|
||||
// Return a promise for the schema with the given name, in Parse format. If
|
||||
// this adapter doesn't know about the schema, return a promise that rejects with
|
||||
// undefined as the reason.
|
||||
getOneSchema(className) {
|
||||
return this.schemaCollection()
|
||||
.then(schemasCollection => schemasCollection._fechOneSchemaFrom_SCHEMA(className));
|
||||
}
|
||||
|
||||
// TODO: As yet not particularly well specified. Creates an object. Shouldn't need the
|
||||
// schemaController, but MongoTransform still needs it :( maybe shouldn't even need the schema,
|
||||
// and should infer from the type. Or maybe does need the schema for validations. Or maybe needs
|
||||
// the schem only for the legacy mongo format. We'll figure that out later.
|
||||
createObject(className, object, schemaController, parseFormatSchema) {
|
||||
const mongoObject = transform.parseObjectToMongoObjectForCreate(schemaController, className, object, parseFormatSchema);
|
||||
return this.adaptiveCollection(className)
|
||||
.then(collection => collection.insertOne(mongoObject));
|
||||
}
|
||||
|
||||
// Remove all objects that match the given parse query. Parse Query should be in Parse Format.
|
||||
// If no objects match, reject with OBJECT_NOT_FOUND. If objects are found and deleted, resolve with undefined.
|
||||
// If there is some other error, reject with INTERNAL_SERVER_ERROR.
|
||||
|
||||
// Currently accepts validate for legacy reasons. Currently accepts the schema, that may not actually be necessary.
|
||||
deleteObjectsByQuery(className, query, validate, schema) {
|
||||
return this.adaptiveCollection(className)
|
||||
.then(collection => {
|
||||
let mongoWhere = transform.transformWhere(className, query, { validate }, schema);
|
||||
return collection.deleteMany(mongoWhere)
|
||||
})
|
||||
.then(({ result }) => {
|
||||
if (result.n === 0) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.');
|
||||
}
|
||||
return Promise.resolve();
|
||||
}, error => {
|
||||
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Database adapter error');
|
||||
});
|
||||
}
|
||||
|
||||
get transform() {
|
||||
return transform;
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@ import _ from 'lodash';
|
||||
var mongodb = require('mongodb');
|
||||
var Parse = require('parse/node').Parse;
|
||||
|
||||
// TODO: Turn this into a helper library for the database adapter.
|
||||
|
||||
// Transforms a key-value pair from REST API form to Mongo form.
|
||||
// This is the main entry point for converting anything from REST form
|
||||
// to Mongo form; no conversion should happen that doesn't pass
|
||||
@@ -13,9 +11,6 @@ var Parse = require('parse/node').Parse;
|
||||
//
|
||||
// There are several options that can help transform:
|
||||
//
|
||||
// query: true indicates that query constraints like $lt are allowed in
|
||||
// the value.
|
||||
//
|
||||
// update: true indicates that __op operators like Add and Delete
|
||||
// in the value are converted to a mongo update form. Otherwise they are
|
||||
// converted to static data.
|
||||
@@ -23,9 +18,12 @@ var Parse = require('parse/node').Parse;
|
||||
// validate: true indicates that key names are to be validated.
|
||||
//
|
||||
// Returns an object with {key: key, value: value}.
|
||||
export function transformKeyValue(schema, className, restKey, restValue, options) {
|
||||
options = options || {};
|
||||
|
||||
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;
|
||||
var timeField = false;
|
||||
@@ -64,47 +62,17 @@ export function transformKeyValue(schema, className, restKey, restValue, options
|
||||
return {key: key, value: restValue};
|
||||
break;
|
||||
case '$or':
|
||||
if (!options.query) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME,
|
||||
'you can only use $or in queries');
|
||||
}
|
||||
if (!(restValue instanceof Array)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY,
|
||||
'bad $or format - use an array value');
|
||||
}
|
||||
var mongoSubqueries = restValue.map((s) => {
|
||||
return transformWhere(schema, className, s);
|
||||
});
|
||||
return {key: '$or', value: mongoSubqueries};
|
||||
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'you can only use $or in queries');
|
||||
case '$and':
|
||||
if (!options.query) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME,
|
||||
'you can only use $and in queries');
|
||||
}
|
||||
if (!(restValue instanceof Array)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY,
|
||||
'bad $and format - use an array value');
|
||||
}
|
||||
var mongoSubqueries = restValue.map((s) => {
|
||||
return transformWhere(schema, className, s);
|
||||
});
|
||||
return {key: '$and', value: mongoSubqueries};
|
||||
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'you can only use $and in queries');
|
||||
default:
|
||||
// Other auth data
|
||||
var authDataMatch = key.match(/^authData\.([a-zA-Z0-9_]+)\.id$/);
|
||||
if (authDataMatch) {
|
||||
if (options.query) {
|
||||
var provider = authDataMatch[1];
|
||||
// Special-case auth data.
|
||||
return {key: '_auth_data_'+provider+'.id', value: restValue};
|
||||
}
|
||||
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME,
|
||||
'can only query on ' + key);
|
||||
break;
|
||||
};
|
||||
if (options.validate && !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME,
|
||||
'invalid key name: ' + key);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,24 +87,10 @@ export function transformKeyValue(schema, className, restKey, restValue, options
|
||||
(!expected && restValue && restValue.__type == 'Pointer')) {
|
||||
key = '_p_' + key;
|
||||
}
|
||||
var inArray = (expected && expected.type === 'Array');
|
||||
|
||||
// Handle query constraints
|
||||
if (options.query) {
|
||||
value = transformConstraint(restValue, inArray);
|
||||
if (value !== CannotTransform) {
|
||||
return {key: key, value: value};
|
||||
}
|
||||
}
|
||||
|
||||
if (inArray && options.query && !(restValue instanceof Array)) {
|
||||
return {
|
||||
key: key, value: { '$all' : [restValue] }
|
||||
};
|
||||
}
|
||||
var expectedTypeIsArray = (expected && expected.type === 'Array');
|
||||
|
||||
// Handle atomic values
|
||||
var value = transformAtom(restValue, false, options);
|
||||
var value = transformAtom(restValue, false, { inArray, inObject });
|
||||
if (value !== CannotTransform) {
|
||||
if (timeField && (typeof value === 'string')) {
|
||||
value = new Date(value);
|
||||
@@ -152,10 +106,6 @@ export function transformKeyValue(schema, className, restKey, restValue, options
|
||||
|
||||
// Handle arrays
|
||||
if (restValue instanceof Array) {
|
||||
if (options.query) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON,
|
||||
'cannot use array as query param');
|
||||
}
|
||||
value = restValue.map((restObj) => {
|
||||
var out = transformKeyValue(schema, className, restKey, restObj, { inArray: true });
|
||||
return out.value;
|
||||
@@ -164,7 +114,7 @@ export function transformKeyValue(schema, className, restKey, restValue, options
|
||||
}
|
||||
|
||||
// Handle update operators
|
||||
value = transformUpdateOperator(restValue, !options.update);
|
||||
value = transformUpdateOperator(restValue, !update);
|
||||
if (value !== CannotTransform) {
|
||||
return {key: key, value: value};
|
||||
}
|
||||
@@ -180,38 +130,214 @@ export function transformKeyValue(schema, className, restKey, restValue, options
|
||||
return {key: key, value: value};
|
||||
}
|
||||
|
||||
const valueAsDate = value => {
|
||||
if (typeof value === 'string') {
|
||||
return new Date(value);
|
||||
} else if (value instanceof Date) {
|
||||
return value;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function transformQueryKeyValue(className, key, value, { validate } = {}, schema) {
|
||||
switch(key) {
|
||||
case 'createdAt':
|
||||
if (valueAsDate(value)) {
|
||||
return {key: '_created_at', value: valueAsDate(value)}
|
||||
}
|
||||
key = '_created_at';
|
||||
break;
|
||||
case 'updatedAt':
|
||||
if (valueAsDate(value)) {
|
||||
return {key: '_updated_at', value: valueAsDate(value)}
|
||||
}
|
||||
key = '_updated_at';
|
||||
break;
|
||||
case 'expiresAt':
|
||||
if (valueAsDate(value)) {
|
||||
return {key: 'expiresAt', value: valueAsDate(value)}
|
||||
}
|
||||
break;
|
||||
case 'objectId': return {key: '_id', value}
|
||||
case 'sessionToken': return {key: '_session_token', value}
|
||||
case '_rperm':
|
||||
case '_wperm':
|
||||
case '_perishable_token':
|
||||
case '_email_verify_token': return {key, value}
|
||||
case '$or':
|
||||
if (!(value instanceof Array)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'bad $or format - use an array value');
|
||||
}
|
||||
return {key: '$or', value: value.map(subQuery => transformWhere(className, subQuery, {}, schema))};
|
||||
case '$and':
|
||||
if (!(value instanceof Array)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'bad $and format - use an array value');
|
||||
}
|
||||
return {key: '$and', value: value.map(subQuery => transformWhere(className, subQuery, {}, schema))};
|
||||
default:
|
||||
// Other auth data
|
||||
const authDataMatch = key.match(/^authData\.([a-zA-Z0-9_]+)\.id$/);
|
||||
if (authDataMatch) {
|
||||
const provider = authDataMatch[1];
|
||||
// Special-case auth data.
|
||||
return {key: `_auth_data_${provider}.id`, value};
|
||||
}
|
||||
if (validate && !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'invalid key name: ' + key);
|
||||
}
|
||||
}
|
||||
|
||||
const expectedTypeIsArray =
|
||||
schema &&
|
||||
schema.fields[key] &&
|
||||
schema.fields[key].type === 'Array';
|
||||
|
||||
const expectedTypeIsPointer =
|
||||
schema &&
|
||||
schema.fields[key] &&
|
||||
schema.fields[key].type === 'Pointer';
|
||||
|
||||
if (expectedTypeIsPointer || !schema && value && value.__type === 'Pointer') {
|
||||
key = '_p_' + key;
|
||||
}
|
||||
|
||||
// Handle query constraints
|
||||
if (transformConstraint(value, expectedTypeIsArray) !== CannotTransform) {
|
||||
return {key, value: transformConstraint(value, expectedTypeIsArray)};
|
||||
}
|
||||
|
||||
if (expectedTypeIsArray && !(value instanceof Array)) {
|
||||
return {key, value: { '$all' : [value] }};
|
||||
}
|
||||
|
||||
// Handle atomic values
|
||||
if (transformAtom(value, false) !== CannotTransform) {
|
||||
return {key, value: transformAtom(value, false)};
|
||||
} else {
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON, `You cannot use ${value} as a query parameter.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Main exposed method to help run queries.
|
||||
// restWhere is the "where" clause in REST API form.
|
||||
// Returns the mongo form of the query.
|
||||
// Throws a Parse.Error if the input query is invalid.
|
||||
function transformWhere(schema, className, restWhere, options = {validate: true}) {
|
||||
function transformWhere(className, restWhere, { validate = true } = {}, schema) {
|
||||
let mongoWhere = {};
|
||||
if (restWhere['ACL']) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.');
|
||||
}
|
||||
let transformKeyOptions = {query: true};
|
||||
transformKeyOptions.validate = options.validate;
|
||||
for (let restKey in restWhere) {
|
||||
let out = transformKeyValue(schema, className, restKey, restWhere[restKey],
|
||||
transformKeyOptions);
|
||||
let out = transformQueryKeyValue(className, restKey, restWhere[restKey], { validate }, schema);
|
||||
mongoWhere[out.key] = out.value;
|
||||
}
|
||||
return mongoWhere;
|
||||
}
|
||||
|
||||
const parseObjectKeyValueToMongoObjectKeyValue = (
|
||||
schema,
|
||||
className,
|
||||
restKey,
|
||||
restValue,
|
||||
parseFormatSchema
|
||||
) => {
|
||||
// Check if the schema is known since it's a built-in field.
|
||||
let transformedValue;
|
||||
let coercedToDate;
|
||||
switch(restKey) {
|
||||
case 'objectId': return {key: '_id', value: restValue};
|
||||
case 'createdAt':
|
||||
transformedValue = transformAtom(restValue, false);
|
||||
coercedToDate = typeof transformedValue === 'string' ? new Date(transformedValue) : transformedValue
|
||||
return {key: '_created_at', value: coercedToDate};
|
||||
case 'updatedAt':
|
||||
transformedValue = transformAtom(restValue, false);
|
||||
coercedToDate = typeof transformedValue === 'string' ? new Date(transformedValue) : transformedValue
|
||||
return {key: '_updated_at', value: coercedToDate};
|
||||
case 'expiresAt':
|
||||
transformedValue = transformAtom(restValue, false);
|
||||
coercedToDate = typeof transformedValue === 'string' ? new Date(transformedValue) : transformedValue
|
||||
return {key: 'expiresAt', value: coercedToDate};
|
||||
case '_rperm':
|
||||
case '_wperm':
|
||||
case '_email_verify_token':
|
||||
case '_hashed_password':
|
||||
case '_perishable_token': return {key: restKey, value: restValue};
|
||||
case 'sessionToken': return {key: '_session_token', value: restValue};
|
||||
default:
|
||||
// Auth data should have been transformed already
|
||||
if (restKey.match(/^authData\.([a-zA-Z0-9_]+)\.id$/)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'can only query on ' + restKey);
|
||||
}
|
||||
// Trust that the auth data has been transformed and save it directly
|
||||
if (restKey.match(/^_auth_data_[a-zA-Z0-9_]+$/)) {
|
||||
return {key: restKey, value: restValue};
|
||||
}
|
||||
}
|
||||
//skip straight to transformAtom for Bytes, they don't show up in the schema for some reason
|
||||
if (restValue && restValue.__type !== 'Bytes') {
|
||||
//Note: We may not know the type of a field here, as the user could be saving (null) to a field
|
||||
//That never existed before, meaning we can't infer the type.
|
||||
if (parseFormatSchema.fields[restKey] && parseFormatSchema.fields[restKey].type == 'Pointer' || restValue.__type == 'Pointer') {
|
||||
restKey = '_p_' + restKey;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle atomic values
|
||||
var value = transformAtom(restValue, false, { inArray: false, inObject: false });
|
||||
if (value !== CannotTransform) {
|
||||
return {key: restKey, value: value};
|
||||
}
|
||||
|
||||
// ACLs are handled before this method is called
|
||||
// If an ACL key still exists here, something is wrong.
|
||||
if (restKey === 'ACL') {
|
||||
throw 'There was a problem transforming an ACL.';
|
||||
}
|
||||
|
||||
// Handle arrays
|
||||
if (restValue instanceof Array) {
|
||||
value = restValue.map((restObj) => {
|
||||
var out = transformKeyValue(schema, className, restKey, restObj, { inArray: true });
|
||||
return out.value;
|
||||
});
|
||||
return {key: restKey, value: value};
|
||||
}
|
||||
|
||||
// Handle update operators. TODO: handle within Parse Server. DB adapter shouldn't see update operators in creates.
|
||||
value = transformUpdateOperator(restValue, true);
|
||||
if (value !== CannotTransform) {
|
||||
return {key: restKey, value: value};
|
||||
}
|
||||
|
||||
// Handle normal objects by recursing
|
||||
value = {};
|
||||
for (var subRestKey in restValue) {
|
||||
var subRestValue = restValue[subRestKey];
|
||||
var out = transformKeyValue(schema, className, subRestKey, subRestValue, { inObject: true });
|
||||
// For recursed objects, keep the keys in rest format
|
||||
value[subRestKey] = out.value;
|
||||
}
|
||||
return {key: restKey, value: value};
|
||||
}
|
||||
|
||||
// Main exposed method to create new objects.
|
||||
// restCreate is the "create" clause in REST API form.
|
||||
// Returns the mongo form of the object.
|
||||
function transformCreate(schema, className, restCreate) {
|
||||
function parseObjectToMongoObjectForCreate(schema, className, restCreate, parseFormatSchema) {
|
||||
if (className == '_User') {
|
||||
restCreate = transformAuthData(restCreate);
|
||||
}
|
||||
var mongoCreate = transformACL(restCreate);
|
||||
for (var restKey in restCreate) {
|
||||
var out = transformKeyValue(schema, className, restKey, restCreate[restKey]);
|
||||
if (out.value !== undefined) {
|
||||
mongoCreate[out.key] = out.value;
|
||||
for (let restKey in restCreate) {
|
||||
let { key, value } = parseObjectKeyValueToMongoObjectKeyValue(
|
||||
schema,
|
||||
className,
|
||||
restKey,
|
||||
restCreate[restKey],
|
||||
parseFormatSchema
|
||||
);
|
||||
if (value !== undefined) {
|
||||
mongoCreate[key] = value;
|
||||
}
|
||||
}
|
||||
return mongoCreate;
|
||||
@@ -239,8 +365,7 @@ function transformUpdate(schema, className, restUpdate) {
|
||||
}
|
||||
|
||||
for (var restKey in restUpdate) {
|
||||
var out = transformKeyValue(schema, className, restKey, restUpdate[restKey],
|
||||
{update: true});
|
||||
var out = transformKeyValue(schema, className, restKey, restUpdate[restKey], {update: true});
|
||||
|
||||
// If the output value is an object with any $ keys, it's an
|
||||
// operator that needs to be lifted onto the top level update
|
||||
@@ -348,23 +473,20 @@ function CannotTransform() {}
|
||||
// Raises an error if this cannot possibly be valid REST format.
|
||||
// Returns CannotTransform if it's just not an atom, or if force is
|
||||
// true, throws an error.
|
||||
function transformAtom(atom, force, options) {
|
||||
options = options || {};
|
||||
var inArray = options.inArray;
|
||||
var inObject = options.inObject;
|
||||
function transformAtom(atom, force, {
|
||||
inArray,
|
||||
inObject,
|
||||
} = {}) {
|
||||
switch(typeof atom) {
|
||||
case 'string':
|
||||
case 'number':
|
||||
case 'boolean':
|
||||
return atom;
|
||||
|
||||
case 'undefined':
|
||||
return atom;
|
||||
case 'symbol':
|
||||
case 'function':
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON,
|
||||
'cannot transform value: ' + atom);
|
||||
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON, `cannot transform value: ${atom}`);
|
||||
case 'object':
|
||||
if (atom instanceof Date) {
|
||||
// Technically dates are not rest format, but, it seems pretty
|
||||
@@ -379,7 +501,7 @@ function transformAtom(atom, force, options) {
|
||||
// TODO: check validity harder for the __type-defined types
|
||||
if (atom.__type == 'Pointer') {
|
||||
if (!inArray && !inObject) {
|
||||
return atom.className + '$' + atom.objectId;
|
||||
return `${atom.className}$${atom.objectId}`;
|
||||
}
|
||||
return {
|
||||
__type: 'Pointer',
|
||||
@@ -404,15 +526,13 @@ function transformAtom(atom, force, options) {
|
||||
}
|
||||
|
||||
if (force) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON,
|
||||
'bad atom: ' + atom);
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON, `bad atom: ${atom}`);
|
||||
}
|
||||
return CannotTransform;
|
||||
|
||||
default:
|
||||
// I don't think typeof can ever let us get here
|
||||
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR,
|
||||
'really did not expect value: ' + atom);
|
||||
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, `really did not expect value: ${atom}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -829,27 +949,6 @@ function transformNotInQuery(notInQueryObject, className, results) {
|
||||
}
|
||||
}
|
||||
|
||||
function addWriteACL(mongoWhere, acl) {
|
||||
var writePerms = [
|
||||
{_wperm: {'$exists': false}}
|
||||
];
|
||||
for (var entry of acl) {
|
||||
writePerms.push({_wperm: {'$in': [entry]}});
|
||||
}
|
||||
return {'$and': [mongoWhere, {'$or': writePerms}]};
|
||||
}
|
||||
|
||||
function addReadACL(mongoWhere, acl) {
|
||||
var orParts = [
|
||||
{"_rperm" : { "$exists": false }},
|
||||
{"_rperm" : { "$in" : ["*"]}}
|
||||
];
|
||||
for (var entry of acl) {
|
||||
orParts.push({"_rperm" : { "$in" : [entry]}});
|
||||
}
|
||||
return {'$and': [mongoWhere, {'$or': orParts}]};
|
||||
}
|
||||
|
||||
var DateCoder = {
|
||||
JSONToDatabase(json) {
|
||||
return new Date(json.iso);
|
||||
@@ -940,14 +1039,12 @@ var FileCoder = {
|
||||
|
||||
module.exports = {
|
||||
transformKey,
|
||||
transformCreate,
|
||||
parseObjectToMongoObjectForCreate,
|
||||
transformUpdate,
|
||||
transformWhere,
|
||||
transformSelect,
|
||||
transformDontSelect,
|
||||
transformInQuery,
|
||||
transformNotInQuery,
|
||||
addReadACL,
|
||||
addWriteACL,
|
||||
untransformObject
|
||||
};
|
||||
|
||||
+1
-5
@@ -52,11 +52,7 @@ var getAuthForSessionToken = function({ config, sessionToken, installationId } =
|
||||
limit: 1,
|
||||
include: 'user'
|
||||
};
|
||||
var restWhere = {
|
||||
_session_token: sessionToken
|
||||
};
|
||||
var query = new RestQuery(config, master(config), '_Session',
|
||||
restWhere, restOptions);
|
||||
var query = new RestQuery(config, master(config), '_Session', { sessionToken }, restOptions);
|
||||
return query.execute().then((response) => {
|
||||
var results = response.results;
|
||||
if (results.length !== 1 || !results[0]['user']) {
|
||||
|
||||
+25
-11
@@ -48,20 +48,29 @@ export class Config {
|
||||
this.mount = removeTrailingSlash(mount);
|
||||
this.liveQueryController = cacheInfo.liveQueryController;
|
||||
this.sessionLength = cacheInfo.sessionLength;
|
||||
this.expireInactiveSessions = cacheInfo.expireInactiveSessions;
|
||||
this.generateSessionExpiresAt = this.generateSessionExpiresAt.bind(this);
|
||||
this.revokeSessionOnPasswordReset = cacheInfo.revokeSessionOnPasswordReset;
|
||||
}
|
||||
|
||||
static validate(options) {
|
||||
this.validateEmailConfiguration({verifyUserEmails: options.verifyUserEmails,
|
||||
appName: options.appName,
|
||||
publicServerURL: options.publicServerURL})
|
||||
this.validateEmailConfiguration({
|
||||
verifyUserEmails: options.verifyUserEmails,
|
||||
appName: options.appName,
|
||||
publicServerURL: options.publicServerURL
|
||||
})
|
||||
|
||||
if (typeof options.revokeSessionOnPasswordReset !== 'boolean') {
|
||||
throw 'revokeSessionOnPasswordReset must be a boolean value';
|
||||
}
|
||||
|
||||
if (options.publicServerURL) {
|
||||
if (!options.publicServerURL.startsWith("http://") && !options.publicServerURL.startsWith("https://")) {
|
||||
throw "publicServerURL should be a valid HTTPS URL starting with https://"
|
||||
}
|
||||
}
|
||||
|
||||
this.validateSessionLength(options.sessionLength);
|
||||
this.validateSessionConfiguration(options.sessionLength, options.expireInactiveSessions);
|
||||
}
|
||||
|
||||
static validateEmailConfiguration({verifyUserEmails, appName, publicServerURL}) {
|
||||
@@ -87,16 +96,21 @@ export class Config {
|
||||
this._mount = newValue;
|
||||
}
|
||||
|
||||
static validateSessionLength(sessionLength) {
|
||||
if(isNaN(sessionLength)) {
|
||||
throw 'Session length must be a valid number.';
|
||||
}
|
||||
else if(sessionLength <= 0) {
|
||||
throw 'Session length must be a value greater than 0.'
|
||||
static validateSessionConfiguration(sessionLength, expireInactiveSessions) {
|
||||
if (expireInactiveSessions) {
|
||||
if (isNaN(sessionLength)) {
|
||||
throw 'Session length must be a valid number.';
|
||||
}
|
||||
else if (sessionLength <= 0) {
|
||||
throw 'Session length must be a value greater than 0.'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
generateSessionExpiresAt() {
|
||||
if (!this.expireInactiveSessions) {
|
||||
return undefined;
|
||||
}
|
||||
var now = new Date();
|
||||
return new Date(now.getTime() + (this.sessionLength*1000));
|
||||
}
|
||||
@@ -124,7 +138,7 @@ export class Config {
|
||||
get verifyEmailURL() {
|
||||
return `${this.publicServerURL}/apps/${this.applicationId}/verify_email`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default Config;
|
||||
module.exports = Config;
|
||||
|
||||
@@ -2,13 +2,28 @@
|
||||
// Parse database.
|
||||
|
||||
import intersect from 'intersect';
|
||||
import _ from 'lodash';
|
||||
|
||||
var mongodb = require('mongodb');
|
||||
var Parse = require('parse/node').Parse;
|
||||
|
||||
var Schema = require('./../Schema');
|
||||
var SchemaController = require('../Controllers/SchemaController');
|
||||
const deepcopy = require('deepcopy');
|
||||
|
||||
function addWriteACL(query, acl) {
|
||||
let newQuery = _.cloneDeep(query);
|
||||
//Can't be any existing '_wperm' query, we don't allow client queries on that, no need to $and
|
||||
newQuery._wperm = { "$in" : [null, ...acl]};
|
||||
return newQuery;
|
||||
}
|
||||
|
||||
function addReadACL(query, acl) {
|
||||
let newQuery = _.cloneDeep(query);
|
||||
//Can't be any existing '_rperm' query, we don't allow client queries on that, no need to $and
|
||||
newQuery._rperm = { "$in" : [null, "*", ...acl]};
|
||||
return newQuery;
|
||||
}
|
||||
|
||||
function DatabaseController(adapter, { skipValidation } = {}) {
|
||||
this.adapter = adapter;
|
||||
|
||||
@@ -44,15 +59,11 @@ DatabaseController.prototype.collectionExists = function(className) {
|
||||
return this.adapter.collectionExists(className);
|
||||
};
|
||||
|
||||
DatabaseController.prototype.dropCollection = function(className) {
|
||||
return this.adapter.dropCollection(className);
|
||||
};
|
||||
|
||||
DatabaseController.prototype.validateClassName = function(className) {
|
||||
if (this.skipValidation) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (!Schema.classNameIsValid(className)) {
|
||||
if (!SchemaController.classNameIsValid(className)) {
|
||||
const error = new Parse.Error(Parse.Error.INVALID_CLASS_NAME, 'invalid className: ' + className);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
@@ -62,26 +73,15 @@ DatabaseController.prototype.validateClassName = function(className) {
|
||||
// Returns a promise for a schema object.
|
||||
// If we are provided a acceptor, then we run it on the schema.
|
||||
// If the schema isn't accepted, we reload it at most once.
|
||||
DatabaseController.prototype.loadSchema = function(acceptor = () => true) {
|
||||
DatabaseController.prototype.loadSchema = function() {
|
||||
|
||||
if (!this.schemaPromise) {
|
||||
this.schemaPromise = this.schemaCollection().then(collection => {
|
||||
delete this.schemaPromise;
|
||||
return Schema.load(collection);
|
||||
return SchemaController.load(collection, this.adapter);
|
||||
});
|
||||
return this.schemaPromise;
|
||||
}
|
||||
|
||||
return this.schemaPromise.then((schema) => {
|
||||
if (acceptor(schema)) {
|
||||
return schema;
|
||||
}
|
||||
this.schemaPromise = this.schemaCollection().then(collection => {
|
||||
delete this.schemaPromise;
|
||||
return Schema.load(collection);
|
||||
});
|
||||
return this.schemaPromise;
|
||||
});
|
||||
return this.schemaPromise;
|
||||
};
|
||||
|
||||
// Returns a promise for the classname that is related to the given
|
||||
@@ -102,10 +102,10 @@ DatabaseController.prototype.redirectClassNameForKey = function(className, key)
|
||||
// Returns a promise that resolves to the new schema.
|
||||
// This does not update this.schema, because in a situation like a
|
||||
// batch request, that could confuse other users of the schema.
|
||||
DatabaseController.prototype.validateObject = function(className, object, query, options) {
|
||||
DatabaseController.prototype.validateObject = function(className, object, query, { acl }) {
|
||||
let schema;
|
||||
let isMaster = !('acl' in options);
|
||||
var aclGroup = options.acl || [];
|
||||
let isMaster = acl === undefined;
|
||||
var aclGroup = acl || [];
|
||||
return this.loadSchema().then(s => {
|
||||
schema = s;
|
||||
if (isMaster) {
|
||||
@@ -127,13 +127,14 @@ DatabaseController.prototype.untransformObject = function(
|
||||
return object;
|
||||
}
|
||||
|
||||
delete object.authData;
|
||||
delete object.sessionToken;
|
||||
|
||||
if (isMaster || (aclGroup.indexOf(object.objectId) > -1)) {
|
||||
return object;
|
||||
}
|
||||
|
||||
delete object.authData;
|
||||
|
||||
return object;
|
||||
};
|
||||
|
||||
@@ -145,52 +146,70 @@ DatabaseController.prototype.untransformObject = function(
|
||||
// acl: a list of strings. If the object to be updated has an ACL,
|
||||
// one of the provided strings must provide the caller with
|
||||
// write permissions.
|
||||
DatabaseController.prototype.update = function(className, query, update, options = {}) {
|
||||
DatabaseController.prototype.update = function(className, query, update, {
|
||||
acl,
|
||||
many,
|
||||
upsert,
|
||||
} = {}) {
|
||||
|
||||
const originalUpdate = update;
|
||||
// Make a copy of the object, so we don't mutate the incoming data.
|
||||
update = deepcopy(update);
|
||||
|
||||
var acceptor = function(schema) {
|
||||
return schema.hasKeys(className, Object.keys(query));
|
||||
};
|
||||
var isMaster = !('acl' in options);
|
||||
var aclGroup = options.acl || [];
|
||||
var mongoUpdate, schema;
|
||||
return this.loadSchema(acceptor)
|
||||
.then(s => {
|
||||
schema = s;
|
||||
if (!isMaster) {
|
||||
return schema.validatePermission(className, aclGroup, 'update');
|
||||
}
|
||||
return Promise.resolve();
|
||||
})
|
||||
var isMaster = acl === undefined;
|
||||
var aclGroup = acl || [];
|
||||
var mongoUpdate;
|
||||
return this.loadSchema()
|
||||
.then(schemaController => {
|
||||
return (isMaster ? Promise.resolve() : schemaController.validatePermission(className, aclGroup, 'update'))
|
||||
.then(() => this.handleRelationUpdates(className, query.objectId, update))
|
||||
.then(() => this.adapter.adaptiveCollection(className))
|
||||
.then(collection => {
|
||||
var mongoWhere = this.transform.transformWhere(schema, className, query, {validate: !this.skipValidation});
|
||||
if (options.acl) {
|
||||
mongoWhere = this.transform.addWriteACL(mongoWhere, options.acl);
|
||||
if (!isMaster) {
|
||||
query = this.addPointerPermissions(schemaController, className, 'update', query, aclGroup);
|
||||
}
|
||||
mongoUpdate = this.transform.transformUpdate(schema, className, update, {validate: !this.skipValidation});
|
||||
if (options.many) {
|
||||
return collection.updateMany(mongoWhere, mongoUpdate);
|
||||
}else if (options.upsert) {
|
||||
return collection.upsertOne(mongoWhere, mongoUpdate);
|
||||
} else {
|
||||
return collection.findOneAndUpdate(mongoWhere, mongoUpdate);
|
||||
if (!query) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (acl) {
|
||||
query = addWriteACL(query, acl);
|
||||
}
|
||||
return schemaController.getOneSchema(className)
|
||||
.catch(error => {
|
||||
// If the schema doesn't exist, pretend it exists with no fields. This behaviour
|
||||
// will likely need revisiting.
|
||||
if (error === undefined) {
|
||||
return { fields: {} };
|
||||
}
|
||||
throw error;
|
||||
})
|
||||
.then(parseFormatSchema => {
|
||||
var mongoWhere = this.transform.transformWhere(className, query, {validate: !this.skipValidation}, parseFormatSchema);
|
||||
mongoUpdate = this.transform.transformUpdate(
|
||||
schemaController,
|
||||
className,
|
||||
update,
|
||||
{validate: !this.skipValidation}
|
||||
);
|
||||
if (many) {
|
||||
return collection.updateMany(mongoWhere, mongoUpdate);
|
||||
} else if (upsert) {
|
||||
return collection.upsertOne(mongoWhere, mongoUpdate);
|
||||
} else {
|
||||
return collection.findOneAndUpdate(mongoWhere, mongoUpdate);
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(result => {
|
||||
if (!result) {
|
||||
return Promise.reject(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Object not found.'));
|
||||
return Promise.reject(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.'));
|
||||
}
|
||||
if (this.skipValidation) {
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
return sanitizeDatabaseResult(originalUpdate, result);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function sanitizeDatabaseResult(originalObject, result) {
|
||||
@@ -214,9 +233,7 @@ function sanitizeDatabaseResult(originalObject, result) {
|
||||
// Returns a promise that resolves successfully when these are
|
||||
// processed.
|
||||
// This mutates update.
|
||||
DatabaseController.prototype.handleRelationUpdates = function(className,
|
||||
objectId,
|
||||
update) {
|
||||
DatabaseController.prototype.handleRelationUpdates = function(className, objectId, update) {
|
||||
var pending = [];
|
||||
var deleteMe = [];
|
||||
objectId = update.objectId || objectId;
|
||||
@@ -293,65 +310,65 @@ DatabaseController.prototype.removeRelation = function(key, fromClassName, fromI
|
||||
// acl: a list of strings. If the object to be updated has an ACL,
|
||||
// one of the provided strings must provide the caller with
|
||||
// write permissions.
|
||||
DatabaseController.prototype.destroy = function(className, query, options = {}) {
|
||||
var isMaster = !('acl' in options);
|
||||
var aclGroup = options.acl || [];
|
||||
DatabaseController.prototype.destroy = function(className, query, { acl } = {}) {
|
||||
const isMaster = acl === undefined;
|
||||
const aclGroup = acl || [];
|
||||
|
||||
var schema;
|
||||
return this.loadSchema()
|
||||
.then(s => {
|
||||
schema = s;
|
||||
.then(schemaController => {
|
||||
return (isMaster ? Promise.resolve() : schemaController.validatePermission(className, aclGroup, 'delete'))
|
||||
.then(() => {
|
||||
if (!isMaster) {
|
||||
return schema.validatePermission(className, aclGroup, 'delete');
|
||||
query = this.addPointerPermissions(schemaController, className, 'delete', query, aclGroup);
|
||||
if (!query) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.');
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
})
|
||||
.then(() => this.adapter.adaptiveCollection(className))
|
||||
.then(collection => {
|
||||
let mongoWhere = this.transform.transformWhere(schema, className, query, {validate: !this.skipValidation});
|
||||
if (options.acl) {
|
||||
mongoWhere = this.transform.addWriteACL(mongoWhere, options.acl);
|
||||
}
|
||||
return collection.deleteMany(mongoWhere);
|
||||
})
|
||||
.then(resp => {
|
||||
//Check _Session to avoid changing password failed without any session.
|
||||
// TODO: @nlutsenko Stop relying on `result.n`
|
||||
if (resp.result.n === 0 && className !== "_Session") {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.');
|
||||
// delete by query
|
||||
if (acl) {
|
||||
query = addWriteACL(query, acl);
|
||||
}
|
||||
return schemaController.getOneSchema(className)
|
||||
.catch(error => {
|
||||
// If the schema doesn't exist, pretend it exists with no fields. This behaviour
|
||||
// will likely need revisiting.
|
||||
if (error === undefined) {
|
||||
return { fields: {} };
|
||||
}
|
||||
throw error;
|
||||
})
|
||||
.then(parseFormatSchema => this.adapter.deleteObjectsByQuery(className, query, !this.skipValidation, parseFormatSchema))
|
||||
.catch(error => {
|
||||
// When deleting sessions while changing passwords, don't throw an error if they don't have any sessions.
|
||||
if (className === "_Session" && error.code === Parse.Error.OBJECT_NOT_FOUND) {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Inserts an object into the database.
|
||||
// Returns a promise that resolves successfully iff the object saved.
|
||||
DatabaseController.prototype.create = function(className, object, options = {}) {
|
||||
DatabaseController.prototype.create = function(className, object, { acl } = {}) {
|
||||
// Make a copy of the object, so we don't mutate the incoming data.
|
||||
let originalObject = object;
|
||||
object = deepcopy(object);
|
||||
|
||||
var schema;
|
||||
var isMaster = !('acl' in options);
|
||||
var aclGroup = options.acl || [];
|
||||
var isMaster = acl === undefined;
|
||||
var aclGroup = acl || [];
|
||||
|
||||
return this.validateClassName(className)
|
||||
.then(() => this.loadSchema())
|
||||
.then(s => {
|
||||
schema = s;
|
||||
if (!isMaster) {
|
||||
return schema.validatePermission(className, aclGroup, 'create');
|
||||
}
|
||||
return Promise.resolve();
|
||||
})
|
||||
.then(() => this.loadSchema())
|
||||
.then(schemaController => {
|
||||
return (isMaster ? Promise.resolve() : schemaController.validatePermission(className, aclGroup, 'create'))
|
||||
.then(() => this.handleRelationUpdates(className, null, object))
|
||||
.then(() => this.adapter.adaptiveCollection(className))
|
||||
.then(coll => {
|
||||
var mongoObject = this.transform.transformCreate(schema, className, object);
|
||||
return coll.insertOne(mongoObject);
|
||||
})
|
||||
.then(result => {
|
||||
return sanitizeDatabaseResult(originalObject, result.ops[0]);
|
||||
});
|
||||
.then(() => schemaController.enforceClassExists(className))
|
||||
.then(() => schemaController.getOneSchema(className, true))
|
||||
.then(schema => this.adapter.createObject(className, object, schemaController, schema))
|
||||
.then(result => sanitizeDatabaseResult(originalObject, result.ops[0]));
|
||||
})
|
||||
};
|
||||
|
||||
DatabaseController.prototype.canAddField = function(schema, className, object, aclGroup) {
|
||||
@@ -384,11 +401,7 @@ DatabaseController.prototype.mongoFind = function(className, query, options = {}
|
||||
// Returns a promise.
|
||||
DatabaseController.prototype.deleteEverything = function() {
|
||||
this.schemaPromise = null;
|
||||
|
||||
return this.adapter.allCollections().then(collections => {
|
||||
let promises = collections.map(collection => collection.drop());
|
||||
return Promise.all(promises);
|
||||
});
|
||||
return this.adapter.deleteAllSchemas();
|
||||
};
|
||||
|
||||
// Finds the keys in a query. Returns a Set. REST format only
|
||||
@@ -584,76 +597,129 @@ DatabaseController.prototype.addNotInObjectIdsIds = function(ids = null, query)
|
||||
// TODO: make userIds not needed here. The db adapter shouldn't know
|
||||
// anything about users, ideally. Then, improve the format of the ACL
|
||||
// arg to work like the others.
|
||||
DatabaseController.prototype.find = function(className, query, options = {}) {
|
||||
DatabaseController.prototype.find = function(className, query, {
|
||||
skip,
|
||||
limit,
|
||||
acl,
|
||||
sort,
|
||||
count,
|
||||
} = {}) {
|
||||
let mongoOptions = {};
|
||||
if (options.skip) {
|
||||
mongoOptions.skip = options.skip;
|
||||
if (skip) {
|
||||
mongoOptions.skip = skip;
|
||||
}
|
||||
if (options.limit) {
|
||||
mongoOptions.limit = options.limit;
|
||||
if (limit) {
|
||||
mongoOptions.limit = limit;
|
||||
}
|
||||
let isMaster = !('acl' in options);
|
||||
let aclGroup = options.acl || [];
|
||||
let acceptor = schema => schema.hasKeys(className, keysForQuery(query))
|
||||
let schema = null;
|
||||
return this.loadSchema(acceptor).then(s => {
|
||||
schema = s;
|
||||
if (options.sort) {
|
||||
let isMaster = acl === undefined;
|
||||
let aclGroup = acl || [];
|
||||
let op = typeof query.objectId == 'string' && Object.keys(query).length === 1 ? 'get' : 'find';
|
||||
return this.loadSchema()
|
||||
.then(schemaController => {
|
||||
if (sort) {
|
||||
mongoOptions.sort = {};
|
||||
for (let key in options.sort) {
|
||||
let mongoKey = this.transform.transformKey(schema, className, key);
|
||||
mongoOptions.sort[mongoKey] = options.sort[key];
|
||||
for (let key in sort) {
|
||||
let mongoKey = this.transform.transformKey(schemaController, className, key);
|
||||
mongoOptions.sort[mongoKey] = sort[key];
|
||||
}
|
||||
}
|
||||
|
||||
if (!isMaster) {
|
||||
let op = typeof query.objectId == 'string' && Object.keys(query).length === 1 ?
|
||||
'get' :
|
||||
'find';
|
||||
return schema.validatePermission(className, aclGroup, op);
|
||||
}
|
||||
return Promise.resolve();
|
||||
})
|
||||
.then(() => this.reduceRelationKeys(className, query))
|
||||
.then(() => this.reduceInRelation(className, query, schema))
|
||||
.then(() => this.adapter.adaptiveCollection(className))
|
||||
.then(collection => {
|
||||
let mongoWhere = this.transform.transformWhere(schema, className, query);
|
||||
if (!isMaster) {
|
||||
mongoWhere = this.transform.addReadACL(mongoWhere, aclGroup);
|
||||
}
|
||||
if (options.count) {
|
||||
delete mongoOptions.limit;
|
||||
return collection.count(mongoWhere, mongoOptions);
|
||||
} else {
|
||||
return collection.find(mongoWhere, mongoOptions)
|
||||
.then((mongoResults) => {
|
||||
return mongoResults.map((r) => {
|
||||
return this.untransformObject(
|
||||
schema, isMaster, aclGroup, className, r);
|
||||
return (isMaster ? Promise.resolve() : schemaController.validatePermission(className, aclGroup, op))
|
||||
.then(() => this.reduceRelationKeys(className, query))
|
||||
.then(() => this.reduceInRelation(className, query, schemaController))
|
||||
.then(() => this.adapter.adaptiveCollection(className))
|
||||
.then(collection => {
|
||||
if (!isMaster) {
|
||||
query = this.addPointerPermissions(schemaController, className, op, query, aclGroup);
|
||||
}
|
||||
if (!query) {
|
||||
if (op == 'get') {
|
||||
return Promise.reject(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Object not found.'));
|
||||
} else {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
}
|
||||
if (!isMaster) {
|
||||
query = addReadACL(query, aclGroup);
|
||||
}
|
||||
return schemaController.getOneSchema(className)
|
||||
.catch(error => {
|
||||
// If the schema doesn't exist, pretend it exists with no fields. This behaviour
|
||||
// will likely need revisiting.
|
||||
if (error === undefined) {
|
||||
return { fields: {} };
|
||||
}
|
||||
throw error;
|
||||
})
|
||||
.then(parseFormatSchema => {
|
||||
let mongoWhere = this.transform.transformWhere(className, query, {}, parseFormatSchema);
|
||||
if (count) {
|
||||
delete mongoOptions.limit;
|
||||
return collection.count(mongoWhere, mongoOptions);
|
||||
} else {
|
||||
return collection.find(mongoWhere, mongoOptions)
|
||||
.then((mongoResults) => {
|
||||
return mongoResults.map((r) => {
|
||||
return this.untransformObject(schemaController, isMaster, aclGroup, className, r);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
DatabaseController.prototype.deleteSchema = function(className) {
|
||||
return this.collectionExists(className)
|
||||
.then(exist => {
|
||||
if (!exist) {
|
||||
return Promise.resolve();
|
||||
.then(exist => {
|
||||
if (!exist) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this.adapter.adaptiveCollection(className)
|
||||
.then(collection => collection.count())
|
||||
.then(count => {
|
||||
if (count > 0) {
|
||||
throw new Parse.Error(255, `Class ${className} is not empty, contains ${count} objects, cannot drop schema.`);
|
||||
}
|
||||
return this.adapter.adaptiveCollection(className)
|
||||
.then(collection => {
|
||||
return collection.count()
|
||||
.then(count => {
|
||||
if (count > 0) {
|
||||
throw new Parse.Error(255, `Class ${className} is not empty, contains ${count} objects, cannot drop schema.`);
|
||||
}
|
||||
return collection.drop();
|
||||
})
|
||||
})
|
||||
return this.adapter.deleteOneSchema(className);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
DatabaseController.prototype.addPointerPermissions = function(schema, className, operation, query, aclGroup = []) {
|
||||
let perms = schema.perms[className];
|
||||
let field = ['get', 'find'].indexOf(operation) > -1 ? 'readUserFields' : 'writeUserFields';
|
||||
let userACL = aclGroup.filter((acl) => {
|
||||
return acl.indexOf('role:') != 0 && acl != '*';
|
||||
});
|
||||
// the ACL should have exactly 1 user
|
||||
if (perms && perms[field] && perms[field].length > 0) {
|
||||
// No user set return undefined
|
||||
if (userACL.length != 1) {
|
||||
return;
|
||||
}
|
||||
let userId = userACL[0];
|
||||
let userPointer = {
|
||||
"__type": "Pointer",
|
||||
"className": "_User",
|
||||
"objectId": userId
|
||||
};
|
||||
|
||||
let constraints = {};
|
||||
let permFields = perms[field];
|
||||
let ors = permFields.map((key) => {
|
||||
let q = {
|
||||
[key]: userPointer
|
||||
};
|
||||
return {'$and': [q, query]};
|
||||
});
|
||||
if (ors.length > 1) {
|
||||
return {'$or': ors};
|
||||
}
|
||||
return ors[0];
|
||||
} else {
|
||||
return query;
|
||||
}
|
||||
}
|
||||
|
||||
function joinTableName(className, key) {
|
||||
|
||||
@@ -59,7 +59,9 @@ export class HooksController {
|
||||
}
|
||||
|
||||
_removeHooks(query) {
|
||||
return this.database.destroy(DefaultHooksCollectionName, query);
|
||||
return this.database.destroy(DefaultHooksCollectionName, query).then(() => {
|
||||
return Promise.resolve({});
|
||||
});
|
||||
}
|
||||
|
||||
saveHook(hook) {
|
||||
|
||||
@@ -15,8 +15,7 @@
|
||||
// TODO: hide all schema logic inside the database adapter.
|
||||
|
||||
const Parse = require('parse/node').Parse;
|
||||
import MongoSchemaCollection from './Adapters/Storage/Mongo/MongoSchemaCollection';
|
||||
import _ from 'lodash';
|
||||
import _ from 'lodash';
|
||||
|
||||
const defaultColumns = Object.freeze({
|
||||
// Contain the default columns for every parse object type (except _Join collection)
|
||||
@@ -92,7 +91,9 @@ const requiredColumns = Object.freeze({
|
||||
_Role: ["name", "ACL"]
|
||||
});
|
||||
|
||||
const systemClasses = Object.freeze(['_User', '_Installation', '_Role', '_Session', '_Product']);
|
||||
const systemClasses = Object.freeze(['_User', '_Installation', '_Role', '_Session', '_Product', '_PushStatus']);
|
||||
|
||||
const volatileClasses = Object.freeze(['_PushStatus']);
|
||||
|
||||
// 10 alpha numberic chars + uppercase
|
||||
const userIdRegex = /^[a-zA-Z0-9]{10}$/;
|
||||
@@ -113,8 +114,8 @@ function verifyPermissionKey(key) {
|
||||
}
|
||||
}
|
||||
|
||||
const CLPValidKeys = Object.freeze(['find', 'get', 'create', 'update', 'delete', 'addField']);
|
||||
function validateCLP(perms) {
|
||||
const CLPValidKeys = Object.freeze(['find', 'get', 'create', 'update', 'delete', 'addField', 'readUserFields', 'writeUserFields']);
|
||||
function validateCLP(perms, fields) {
|
||||
if (!perms) {
|
||||
return;
|
||||
}
|
||||
@@ -122,6 +123,20 @@ function validateCLP(perms) {
|
||||
if (CLPValidKeys.indexOf(operation) == -1) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON, `${operation} is not a valid operation for class level permissions`);
|
||||
}
|
||||
|
||||
if (operation === 'readUserFields' || operation === 'writeUserFields') {
|
||||
if (!Array.isArray(perms[operation])) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON, `'${perms[operation]}' is not a valid value for class level permissions ${operation}`);
|
||||
} else {
|
||||
perms[operation].forEach((key) => {
|
||||
if (!fields[key] || fields[key].type != 'Pointer' || fields[key].targetClass != '_User') {
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON, `'${key}' is not a valid column for class level pointer permissions ${operation}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(perms[operation]).forEach((key) => {
|
||||
verifyPermissionKey(key);
|
||||
let perm = perms[operation][key];
|
||||
@@ -201,15 +216,27 @@ const fieldTypeIsInvalid = ({ type, targetClass }) => {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const injectDefaultSchema = schema => ({
|
||||
className: schema.className,
|
||||
fields: {
|
||||
...defaultColumns._Default,
|
||||
...(defaultColumns[schema.className] || {}),
|
||||
...schema.fields,
|
||||
},
|
||||
classLevelPermissions: schema.classLevelPermissions,
|
||||
})
|
||||
|
||||
// Stores the entire schema of the app in a weird hybrid format somewhere between
|
||||
// the mongo format and the Parse format. Soon, this will all be Parse format.
|
||||
class Schema {
|
||||
class SchemaController {
|
||||
_collection;
|
||||
_dbAdapter;
|
||||
data;
|
||||
perms;
|
||||
|
||||
constructor(collection) {
|
||||
constructor(collection, databaseAdapter) {
|
||||
this._collection = collection;
|
||||
this._dbAdapter = databaseAdapter;
|
||||
|
||||
// this.data[className][fieldName] tells you the type of that field, in mongo format
|
||||
this.data = {};
|
||||
@@ -220,19 +247,37 @@ class Schema {
|
||||
reloadData() {
|
||||
this.data = {};
|
||||
this.perms = {};
|
||||
return this._collection.getAllSchemas().then(allSchemas => {
|
||||
return this.getAllSchemas()
|
||||
.then(allSchemas => {
|
||||
allSchemas.forEach(schema => {
|
||||
const parseFormatSchema = {
|
||||
...defaultColumns._Default,
|
||||
...(defaultColumns[schema.className] || {}),
|
||||
...schema.fields,
|
||||
}
|
||||
this.data[schema.className] = parseFormatSchema;
|
||||
this.data[schema.className] = schema.fields;
|
||||
this.perms[schema.className] = schema.classLevelPermissions;
|
||||
});
|
||||
|
||||
// Inject the in-memory classes
|
||||
volatileClasses.forEach(className => {
|
||||
this.data[className] = injectDefaultSchema({
|
||||
className,
|
||||
fields: {},
|
||||
classLevelPermissions: {}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getAllSchemas() {
|
||||
return this._dbAdapter.getAllSchemas()
|
||||
.then(allSchemas => allSchemas.map(injectDefaultSchema));
|
||||
}
|
||||
|
||||
getOneSchema(className, allowVolatileClasses = false) {
|
||||
if (allowVolatileClasses && volatileClasses.indexOf(className) > -1) {
|
||||
return Promise.resolve(this.data[className]);
|
||||
}
|
||||
return this._dbAdapter.getOneSchema(className)
|
||||
.then(injectDefaultSchema);
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -247,9 +292,6 @@ class Schema {
|
||||
}
|
||||
|
||||
return this._collection.addSchema(className, fields, classLevelPermissions)
|
||||
.then(res => {
|
||||
return Promise.resolve(res);
|
||||
})
|
||||
.catch(error => {
|
||||
if (error === undefined) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`);
|
||||
@@ -260,40 +302,42 @@ class Schema {
|
||||
}
|
||||
|
||||
updateClass(className, submittedFields, classLevelPermissions, database) {
|
||||
if (!this.data[className]) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} does not exist.`);
|
||||
}
|
||||
let existingFields = Object.assign(this.data[className], {_id: className});
|
||||
Object.keys(submittedFields).forEach(name => {
|
||||
let field = submittedFields[name];
|
||||
if (existingFields[name] && field.__op !== 'Delete') {
|
||||
throw new Parse.Error(255, `Field ${name} exists, cannot update.`);
|
||||
return this.hasClass(className)
|
||||
.then(hasClass => {
|
||||
if (!hasClass) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} does not exist.`);
|
||||
}
|
||||
if (!existingFields[name] && field.__op === 'Delete') {
|
||||
throw new Parse.Error(255, `Field ${name} does not exist, cannot delete.`);
|
||||
let existingFields = Object.assign(this.data[className], {_id: className});
|
||||
Object.keys(submittedFields).forEach(name => {
|
||||
let field = submittedFields[name];
|
||||
if (existingFields[name] && field.__op !== 'Delete') {
|
||||
throw new Parse.Error(255, `Field ${name} exists, cannot update.`);
|
||||
}
|
||||
if (!existingFields[name] && field.__op === 'Delete') {
|
||||
throw new Parse.Error(255, `Field ${name} does not exist, cannot delete.`);
|
||||
}
|
||||
});
|
||||
|
||||
let newSchema = buildMergedSchemaObject(existingFields, submittedFields);
|
||||
let validationError = this.validateSchemaData(className, newSchema, classLevelPermissions);
|
||||
if (validationError) {
|
||||
throw new Parse.Error(validationError.code, validationError.error);
|
||||
}
|
||||
});
|
||||
|
||||
let newSchema = buildMergedSchemaObject(existingFields, submittedFields);
|
||||
let validationError = this.validateSchemaData(className, newSchema, classLevelPermissions);
|
||||
if (validationError) {
|
||||
throw new Parse.Error(validationError.code, validationError.error);
|
||||
}
|
||||
// Finally we have checked to make sure the request is valid and we can start deleting fields.
|
||||
// Do all deletions first, then a single save to _SCHEMA collection to handle all additions.
|
||||
let deletePromises = [];
|
||||
let insertedFields = [];
|
||||
Object.keys(submittedFields).forEach(fieldName => {
|
||||
if (submittedFields[fieldName].__op === 'Delete') {
|
||||
const promise = this.deleteField(fieldName, className, database);
|
||||
deletePromises.push(promise);
|
||||
} else {
|
||||
insertedFields.push(fieldName);
|
||||
}
|
||||
});
|
||||
|
||||
// Finally we have checked to make sure the request is valid and we can start deleting fields.
|
||||
// Do all deletions first, then a single save to _SCHEMA collection to handle all additions.
|
||||
let deletePromises = [];
|
||||
let insertedFields = [];
|
||||
Object.keys(submittedFields).forEach(fieldName => {
|
||||
if (submittedFields[fieldName].__op === 'Delete') {
|
||||
const promise = this.deleteField(fieldName, className, database);
|
||||
deletePromises.push(promise);
|
||||
} else {
|
||||
insertedFields.push(fieldName);
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.all(deletePromises) // Delete Everything
|
||||
return Promise.all(deletePromises) // Delete Everything
|
||||
.then(() => this.reloadData()) // Reload our Schema, so we have all the new values
|
||||
.then(() => {
|
||||
let promises = insertedFields.map(fieldName => {
|
||||
@@ -302,15 +346,14 @@ class Schema {
|
||||
});
|
||||
return Promise.all(promises);
|
||||
})
|
||||
.then(() => {
|
||||
return this.setPermissions(className, classLevelPermissions)
|
||||
})
|
||||
.then(() => this.setPermissions(className, classLevelPermissions, newSchema))
|
||||
//TODO: Move this logic into the database adapter
|
||||
.then(() => {
|
||||
return { className: className,
|
||||
fields: this.data[className],
|
||||
classLevelPermissions: this.perms[className] }
|
||||
});
|
||||
.then(() => ({
|
||||
className: className,
|
||||
fields: this.data[className],
|
||||
classLevelPermissions: this.perms[className]
|
||||
}));
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -326,12 +369,8 @@ class Schema {
|
||||
|
||||
// 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 'freeze' is true, refuse to modify the schema.
|
||||
enforceClassExists(className, freeze) {
|
||||
if (this.data[className]) {
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
@@ -351,7 +390,7 @@ class Schema {
|
||||
return this.reloadData();
|
||||
}).then(() => {
|
||||
// Ensure that the schema now validates
|
||||
return this.validateClassName(className, true);
|
||||
return this.enforceClassExists(className, true);
|
||||
}, () => {
|
||||
// The schema still doesn't validate. Give up
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON, 'schema class name does not revalidate');
|
||||
@@ -400,15 +439,15 @@ class Schema {
|
||||
error: 'currently, only one GeoPoint field may exist in an object. Adding ' + geoPoints[1] + ' when ' + geoPoints[0] + ' already exists.',
|
||||
};
|
||||
}
|
||||
validateCLP(classLevelPermissions);
|
||||
validateCLP(classLevelPermissions, fields);
|
||||
}
|
||||
|
||||
// Sets the Class-level permissions for a given className, which must exist.
|
||||
setPermissions(className, perms) {
|
||||
setPermissions(className, perms, newSchema) {
|
||||
if (typeof perms === 'undefined') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
validateCLP(perms);
|
||||
validateCLP(perms, newSchema);
|
||||
let update = {
|
||||
_metadata: {
|
||||
class_permissions: perms
|
||||
@@ -463,18 +502,11 @@ class Schema {
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
|
||||
if (type === 'GeoPoint') {
|
||||
// Make sure there are not other geopoint fields
|
||||
for (let otherKey in this.data[className]) {
|
||||
if (this.data[className][otherKey].type === 'GeoPoint') {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INCORRECT_TYPE,
|
||||
'there can only be one geopoint field in a class');
|
||||
}
|
||||
}
|
||||
if (typeof type === 'string') {
|
||||
type = { type };
|
||||
}
|
||||
|
||||
return this._collection.updateField(className, fieldName, type).then(() => {
|
||||
return this._collection.addFieldIfNotExists(className, fieldName, type).then(() => {
|
||||
// The update succeeded. Reload the schema
|
||||
return this.reloadData();
|
||||
}, () => {
|
||||
@@ -525,15 +557,7 @@ class Schema {
|
||||
if (this.data[className][fieldName].type == 'Relation') {
|
||||
//For relations, drop the _Join table
|
||||
return database.adapter.deleteFields(className, [fieldName], [])
|
||||
.then(() => database.dropCollection(`_Join:${fieldName}:${className}`))
|
||||
.catch(error => {
|
||||
// 'ns not found' means collection was already gone. Ignore deletion attempt.
|
||||
// TODO: 'ns not found' is a mongo implementation detail. Move it into mongo adapter.
|
||||
if (error.message == 'ns not found') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(error);
|
||||
});
|
||||
.then(() => database.adapter.deleteOneSchema(`_Join:${fieldName}:${className}`));
|
||||
}
|
||||
|
||||
const fieldNames = [fieldName];
|
||||
@@ -547,7 +571,7 @@ class Schema {
|
||||
// valid.
|
||||
validateObject(className, object, query) {
|
||||
let geocount = 0;
|
||||
let promise = this.validateClassName(className);
|
||||
let promise = this.enforceClassExists(className);
|
||||
for (let fieldName in object) {
|
||||
if (object[fieldName] === undefined) {
|
||||
continue;
|
||||
@@ -609,7 +633,8 @@ class Schema {
|
||||
if (!this.perms[className] || !this.perms[className][operation]) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
let perms = this.perms[className][operation];
|
||||
let classPerms = this.perms[className];
|
||||
let perms = classPerms[operation];
|
||||
// Handle the public scenario quickly
|
||||
if (perms['*']) {
|
||||
return Promise.resolve();
|
||||
@@ -621,11 +646,26 @@ class Schema {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
// TODO: Verify correct error code
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
|
||||
|
||||
if (found) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// No matching CLP, let's check the Pointer permissions
|
||||
// And handle those later
|
||||
let permissionField = ['get', 'find'].indexOf(operation) > -1 ? 'readUserFields' : 'writeUserFields';
|
||||
|
||||
// Reject create when write lockdown
|
||||
if (permissionField == 'writeUserFields' && operation == 'create') {
|
||||
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN,
|
||||
'Permission denied for this action.');
|
||||
}
|
||||
|
||||
if (Array.isArray(classPerms[permissionField]) && classPerms[permissionField].length > 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN,
|
||||
'Permission denied for this action.');
|
||||
};
|
||||
|
||||
// Returns the expected type for a className+key combination
|
||||
@@ -637,22 +677,11 @@ class Schema {
|
||||
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.
|
||||
// Checks if a given class is in the schema.
|
||||
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) {
|
||||
let expected = this.getExpectedType(className, key);
|
||||
if (expected && expected.charAt(0) == '*') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
getRelationFields(className) {
|
||||
if (this.data && this.data[className]) {
|
||||
let classData = this.data[className];
|
||||
@@ -672,8 +701,8 @@ class Schema {
|
||||
}
|
||||
|
||||
// Returns a promise for a new Schema.
|
||||
function load(collection) {
|
||||
let schema = new Schema(collection);
|
||||
function load(collection, dbAdapter) {
|
||||
let schema = new SchemaController(collection, dbAdapter);
|
||||
return schema.reloadData().then(() => schema);
|
||||
}
|
||||
|
||||
+9
-3
@@ -1,7 +1,5 @@
|
||||
// ParseServer - open-source compatible API Server for Parse apps
|
||||
|
||||
import 'babel-polyfill';
|
||||
|
||||
var batch = require('./batch'),
|
||||
bodyParser = require('body-parser'),
|
||||
DatabaseAdapter = require('./DatabaseAdapter'),
|
||||
@@ -12,6 +10,10 @@ var batch = require('./batch'),
|
||||
path = require('path'),
|
||||
authDataManager = require('./authDataManager');
|
||||
|
||||
if (!global._babelPolyfill) {
|
||||
require('babel-polyfill');
|
||||
}
|
||||
|
||||
import { logger,
|
||||
configureLogger } from './logger';
|
||||
import cache from './cache';
|
||||
@@ -112,7 +114,9 @@ class ParseServer {
|
||||
},
|
||||
liveQuery = {},
|
||||
sessionLength = 31536000, // 1 Year in seconds
|
||||
expireInactiveSessions = true,
|
||||
verbose = false,
|
||||
revokeSessionOnPasswordReset = true,
|
||||
}) {
|
||||
// Initialize the node client SDK automatically
|
||||
Parse.initialize(appId, javascriptKey || 'unused', masterKey);
|
||||
@@ -184,7 +188,9 @@ class ParseServer {
|
||||
customPages: customPages,
|
||||
maxUploadSize: maxUploadSize,
|
||||
liveQueryController: liveQueryController,
|
||||
sessionLength : Number(sessionLength),
|
||||
sessionLength: Number(sessionLength),
|
||||
expireInactiveSessions: expireInactiveSessions,
|
||||
revokeSessionOnPasswordReset
|
||||
});
|
||||
|
||||
// To maintain compatibility. TODO: Remove in some version that breaks backwards compatability
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
// An object that encapsulates everything we need to run a 'find'
|
||||
// operation, encoded in the REST API format.
|
||||
|
||||
var Schema = require('./Schema');
|
||||
var SchemaController = require('./Controllers/SchemaController');
|
||||
var Parse = require('parse/node').Parse;
|
||||
|
||||
import { default as FilesController } from './Controllers/FilesController';
|
||||
@@ -171,7 +171,7 @@ RestQuery.prototype.redirectClassNameForKey = function() {
|
||||
|
||||
// Validates this operation against the allowClientClassCreation config.
|
||||
RestQuery.prototype.validateClientClassCreation = function() {
|
||||
let sysClass = Schema.systemClasses;
|
||||
let sysClass = SchemaController.systemClasses;
|
||||
if (this.config.allowClientClassCreation === false && !this.auth.isMaster
|
||||
&& sysClass.indexOf(this.className) === -1) {
|
||||
return this.config.database.collectionExists(this.className).then((hasClass) => {
|
||||
|
||||
+40
-38
@@ -3,7 +3,7 @@
|
||||
// This could be either a "create" or an "update".
|
||||
|
||||
import cache from './cache';
|
||||
var Schema = require('./Schema');
|
||||
var SchemaController = require('./Controllers/SchemaController');
|
||||
var deepcopy = require('deepcopy');
|
||||
|
||||
var Auth = require('./Auth');
|
||||
@@ -79,6 +79,8 @@ RestWrite.prototype.execute = function() {
|
||||
return this.expandFilesForExistingObjects();
|
||||
}).then(() => {
|
||||
return this.runDatabaseOperation();
|
||||
}).then(() => {
|
||||
return this.createSessionTokenIfNeeded();
|
||||
}).then(() => {
|
||||
return this.handleFollowup();
|
||||
}).then(() => {
|
||||
@@ -111,7 +113,7 @@ RestWrite.prototype.getUserAndRoleACL = function() {
|
||||
|
||||
// Validates this operation against the allowClientClassCreation config.
|
||||
RestWrite.prototype.validateClientClassCreation = function() {
|
||||
let sysClass = Schema.systemClasses;
|
||||
let sysClass = SchemaController.systemClasses;
|
||||
if (this.config.allowClientClassCreation === false && !this.auth.isMaster
|
||||
&& sysClass.indexOf(this.className) === -1) {
|
||||
return this.config.database.collectionExists(this.className).then((hasClass) => {
|
||||
@@ -160,7 +162,7 @@ RestWrite.prototype.runBeforeTrigger = function() {
|
||||
updatedObject.set(this.sanitizedData());
|
||||
|
||||
return Promise.resolve().then(() => {
|
||||
return triggers.maybeRunTrigger(triggers.Types.beforeSave, this.auth, updatedObject, originalObject, this.config.applicationId);
|
||||
return triggers.maybeRunTrigger(triggers.Types.beforeSave, this.auth, updatedObject, originalObject, this.config);
|
||||
}).then((response) => {
|
||||
if (response && response.object) {
|
||||
this.data = response.object;
|
||||
@@ -316,35 +318,6 @@ RestWrite.prototype.transformUser = function() {
|
||||
|
||||
var promise = Promise.resolve();
|
||||
|
||||
if (!this.query) {
|
||||
var token = 'r:' + cryptoUtils.newToken();
|
||||
this.storage['token'] = token;
|
||||
promise = promise.then(() => {
|
||||
var expiresAt = this.config.generateSessionExpiresAt();
|
||||
var sessionData = {
|
||||
sessionToken: token,
|
||||
user: {
|
||||
__type: 'Pointer',
|
||||
className: '_User',
|
||||
objectId: this.objectId()
|
||||
},
|
||||
createdWith: {
|
||||
'action': 'signup',
|
||||
'authProvider': this.storage['authProvider'] || 'password'
|
||||
},
|
||||
restricted: false,
|
||||
installationId: this.auth.installationId,
|
||||
expiresAt: Parse._encode(expiresAt)
|
||||
};
|
||||
if (this.response && this.response.response) {
|
||||
this.response.response.sessionToken = token;
|
||||
}
|
||||
var create = new RestWrite(this.config, Auth.master(this.config),
|
||||
'_Session', null, sessionData);
|
||||
return create.execute();
|
||||
});
|
||||
}
|
||||
|
||||
// If we're updating a _User object, clear the user cache for the session
|
||||
if (this.query && this.auth.user && this.auth.user.getSessionToken()) {
|
||||
cache.users.remove(this.auth.user.getSessionToken());
|
||||
@@ -412,10 +385,42 @@ RestWrite.prototype.transformUser = function() {
|
||||
});
|
||||
};
|
||||
|
||||
RestWrite.prototype.createSessionTokenIfNeeded = function() {
|
||||
if (this.className !== '_User') {
|
||||
return;
|
||||
}
|
||||
if (this.query) {
|
||||
return;
|
||||
}
|
||||
var token = 'r:' + cryptoUtils.newToken();
|
||||
|
||||
var expiresAt = this.config.generateSessionExpiresAt();
|
||||
var sessionData = {
|
||||
sessionToken: token,
|
||||
user: {
|
||||
__type: 'Pointer',
|
||||
className: '_User',
|
||||
objectId: this.objectId()
|
||||
},
|
||||
createdWith: {
|
||||
'action': 'signup',
|
||||
'authProvider': this.storage['authProvider'] || 'password'
|
||||
},
|
||||
restricted: false,
|
||||
installationId: this.auth.installationId,
|
||||
expiresAt: Parse._encode(expiresAt)
|
||||
};
|
||||
if (this.response && this.response.response) {
|
||||
this.response.response.sessionToken = token;
|
||||
}
|
||||
var create = new RestWrite(this.config, Auth.master(this.config),
|
||||
'_Session', null, sessionData);
|
||||
return create.execute();
|
||||
}
|
||||
|
||||
// Handles any followup logic
|
||||
RestWrite.prototype.handleFollowup = function() {
|
||||
|
||||
if (this.storage && this.storage['clearSessions']) {
|
||||
if (this.storage && this.storage['clearSessions'] && this.config.revokeSessionOnPasswordReset) {
|
||||
var sessionQuery = {
|
||||
user: {
|
||||
__type: 'Pointer',
|
||||
@@ -775,9 +780,6 @@ RestWrite.prototype.runDatabaseOperation = function() {
|
||||
return memo;
|
||||
}, resp);
|
||||
}
|
||||
if (this.storage['token']) {
|
||||
resp.sessionToken = this.storage['token'];
|
||||
}
|
||||
this.response = {
|
||||
status: 201,
|
||||
response: resp,
|
||||
@@ -821,7 +823,7 @@ RestWrite.prototype.runAfterTrigger = function() {
|
||||
this.config.liveQueryController.onAfterSave(updatedObject.className, updatedObject, originalObject);
|
||||
|
||||
// Run afterSave trigger
|
||||
triggers.maybeRunTrigger(triggers.Types.afterSave, this.auth, updatedObject, originalObject, this.config.applicationId);
|
||||
triggers.maybeRunTrigger(triggers.Types.afterSave, this.auth, updatedObject, originalObject, this.config);
|
||||
};
|
||||
|
||||
// A helper to figure out what location this operation happens at.
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import rest from '../rest';
|
||||
import rest from '../rest';
|
||||
|
||||
import url from 'url';
|
||||
import url from 'url';
|
||||
|
||||
const ALLOWED_GET_QUERY_KEYS = ['keys', 'include'];
|
||||
|
||||
export class ClassesRouter extends PromiseRouter {
|
||||
|
||||
|
||||
handleFind(req) {
|
||||
let body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query));
|
||||
let options = {};
|
||||
@@ -16,7 +16,7 @@ export class ClassesRouter extends PromiseRouter {
|
||||
|
||||
for (let key of Object.keys(body)) {
|
||||
if (allowConstraints.indexOf(key) === -1) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Improper encode of parameter');
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, `Invalid paramater for query: ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,18 +82,18 @@ export class ClassesRouter extends PromiseRouter {
|
||||
if (!response.results || response.results.length == 0) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.');
|
||||
}
|
||||
|
||||
|
||||
if (req.params.className === "_User") {
|
||||
|
||||
|
||||
delete response.results[0].sessionToken;
|
||||
|
||||
|
||||
const user = response.results[0];
|
||||
|
||||
|
||||
if (req.auth.user && user.objectId == req.auth.user.id) {
|
||||
// Force the session token
|
||||
response.results[0].sessionToken = req.info.sessionToken;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { response: response.results[0] };
|
||||
});
|
||||
}
|
||||
@@ -124,7 +124,7 @@ export class ClassesRouter extends PromiseRouter {
|
||||
}
|
||||
return json
|
||||
}
|
||||
|
||||
|
||||
mountRoutes() {
|
||||
this.route('GET', '/classes/:className', (req) => { return this.handleFind(req); });
|
||||
this.route('GET', '/classes/:className/:objectId', (req) => { return this.handleGet(req); });
|
||||
|
||||
@@ -7,11 +7,11 @@ var express = require('express'),
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
|
||||
export class FunctionsRouter extends PromiseRouter {
|
||||
|
||||
|
||||
mountRoutes() {
|
||||
this.route('POST', '/functions/:functionName', FunctionsRouter.handleCloudFunction);
|
||||
}
|
||||
|
||||
|
||||
static createResponseObject(resolve, reject) {
|
||||
return {
|
||||
success: function(result) {
|
||||
@@ -26,19 +26,27 @@ export class FunctionsRouter extends PromiseRouter {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static handleCloudFunction(req) {
|
||||
var applicationId = req.config.applicationId;
|
||||
var theFunction = triggers.getFunction(req.params.functionName, applicationId);
|
||||
var theValidator = triggers.getValidator(req.params.functionName, applicationId);
|
||||
if (theFunction) {
|
||||
|
||||
const params = Object.assign({}, req.body, req.query);
|
||||
for (var key in params) {
|
||||
if (params.hasOwnProperty(key)) {
|
||||
var value = params[key];
|
||||
if (value.__type == 'Date') {
|
||||
params[key] = new Date(value.iso);
|
||||
}
|
||||
}
|
||||
}
|
||||
var request = {
|
||||
params: params,
|
||||
master: req.auth && req.auth.isMaster,
|
||||
user: req.auth && req.auth.user,
|
||||
installationId: req.info.installationId
|
||||
installationId: req.info.installationId,
|
||||
log: req.config.loggerController && req.config.loggerController.adapter
|
||||
};
|
||||
|
||||
if (theValidator && typeof theValidator === "function") {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// global_config.js
|
||||
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import * as middleware from "../middlewares";
|
||||
|
||||
export class GlobalConfigRouter extends PromiseRouter {
|
||||
getGlobalConfig(req) {
|
||||
let database = req.config.database.WithoutValidation();
|
||||
return database.find('_GlobalConfig', { '_id': 1 }, { limit: 1 }).then((results) => {
|
||||
return database.find('_GlobalConfig', { objectId: 1 }, { limit: 1 }).then((results) => {
|
||||
if (results.length != 1) {
|
||||
// If there is no config in the database - return empty config.
|
||||
return { response: { params: {} } };
|
||||
|
||||
@@ -6,25 +6,27 @@ import rest from '../rest';
|
||||
|
||||
export class InstallationsRouter extends ClassesRouter {
|
||||
handleFind(req) {
|
||||
let body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query));
|
||||
var options = {};
|
||||
if (req.body.skip) {
|
||||
options.skip = Number(req.body.skip);
|
||||
|
||||
if (body.skip) {
|
||||
options.skip = Number(body.skip);
|
||||
}
|
||||
if (req.body.limit) {
|
||||
options.limit = Number(req.body.limit);
|
||||
if (body.limit) {
|
||||
options.limit = Number(body.limit);
|
||||
}
|
||||
if (req.body.order) {
|
||||
options.order = String(req.body.order);
|
||||
if (body.order) {
|
||||
options.order = String(body.order);
|
||||
}
|
||||
if (req.body.count) {
|
||||
if (body.count) {
|
||||
options.count = true;
|
||||
}
|
||||
if (req.body.include) {
|
||||
options.include = String(req.body.include);
|
||||
if (body.include) {
|
||||
options.include = String(body.include);
|
||||
}
|
||||
|
||||
return rest.find(req.config, req.auth,
|
||||
'_Installation', req.body.where, options)
|
||||
'_Installation', body.where, options)
|
||||
.then((response) => {
|
||||
return {response: response};
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
var express = require('express'),
|
||||
Parse = require('parse/node').Parse,
|
||||
Schema = require('../Schema');
|
||||
SchemaController = require('../Controllers/SchemaController');
|
||||
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import * as middleware from "../middlewares";
|
||||
@@ -14,36 +14,24 @@ function classNameMismatchResponse(bodyClass, pathClass) {
|
||||
);
|
||||
}
|
||||
|
||||
function injectDefaultSchema(schema) {
|
||||
let defaultSchema = Schema.defaultColumns[schema.className];
|
||||
if (defaultSchema) {
|
||||
Object.keys(defaultSchema).forEach((key) => {
|
||||
schema.fields[key] = defaultSchema[key];
|
||||
});
|
||||
}
|
||||
return schema;
|
||||
}
|
||||
|
||||
function getAllSchemas(req) {
|
||||
return req.config.database.schemaCollection()
|
||||
.then(collection => collection.getAllSchemas())
|
||||
.then(schemas => schemas.map(injectDefaultSchema))
|
||||
.then(schemas => ({ response: { results: schemas } }));
|
||||
return req.config.database.loadSchema()
|
||||
.then(schemaController => schemaController.getAllSchemas())
|
||||
.then(schemas => ({ response: { results: schemas } }));
|
||||
}
|
||||
|
||||
function getOneSchema(req) {
|
||||
const className = req.params.className;
|
||||
return req.config.database.schemaCollection()
|
||||
.then(collection => collection.findSchema(className))
|
||||
.then(injectDefaultSchema)
|
||||
.then(schema => ({ response: schema }))
|
||||
.catch(error => {
|
||||
if (error === undefined) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} does not exist.`);
|
||||
} else {
|
||||
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Database adapter error.');
|
||||
}
|
||||
});
|
||||
return req.config.database.loadSchema()
|
||||
.then(schemaController => schemaController.getOneSchema(className))
|
||||
.then(schema => ({ response: schema }))
|
||||
.catch(error => {
|
||||
if (error === undefined) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} does not exist.`);
|
||||
} else {
|
||||
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Database adapter error.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createSchema(req) {
|
||||
@@ -72,19 +60,8 @@ function modifySchema(req) {
|
||||
let className = req.params.className;
|
||||
|
||||
return req.config.database.loadSchema()
|
||||
.then(schema => {
|
||||
return schema.updateClass(className, submittedFields, req.body.classLevelPermissions, req.config.database);
|
||||
}).then((result) => {
|
||||
return Promise.resolve({response: result});
|
||||
});
|
||||
}
|
||||
|
||||
function getSchemaPermissions(req) {
|
||||
var className = req.params.className;
|
||||
return req.config.database.loadSchema()
|
||||
.then(schema => {
|
||||
return Promise.resolve({response: schema.perms[className]});
|
||||
});
|
||||
.then(schema => schema.updateClass(className, submittedFields, req.body.classLevelPermissions, req.config.database))
|
||||
.then(result => ({response: result}));
|
||||
}
|
||||
|
||||
// A helper function that removes all join tables for a schema. Returns a promise.
|
||||
@@ -93,41 +70,28 @@ var removeJoinTables = (database, mongoSchema) => {
|
||||
.filter(field => mongoSchema[field].startsWith('relation<'))
|
||||
.map(field => {
|
||||
let collectionName = `_Join:${field}:${mongoSchema._id}`;
|
||||
return database.dropCollection(collectionName);
|
||||
return database.adapter.deleteOneSchema(collectionName);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
function deleteSchema(req) {
|
||||
if (!Schema.classNameIsValid(req.params.className)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, Schema.invalidClassNameMessage(req.params.className));
|
||||
if (!SchemaController.classNameIsValid(req.params.className)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, SchemaController.invalidClassNameMessage(req.params.className));
|
||||
}
|
||||
|
||||
return req.config.database.deleteSchema(req.params.className)
|
||||
.then(() => {
|
||||
// We've dropped the collection now, so delete the item from _SCHEMA
|
||||
// and clear the _Join collections
|
||||
return req.config.database.schemaCollection()
|
||||
.then(coll => coll.findAndDeleteSchema(req.params.className))
|
||||
.then(document => {
|
||||
if (document === null) {
|
||||
//tried to delete non-existent class
|
||||
return Promise.resolve();
|
||||
}
|
||||
return removeJoinTables(req.config.database, document);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
// Success
|
||||
return { response: {} };
|
||||
}, error => {
|
||||
if (error.message == 'ns not found') {
|
||||
// If they try to delete a non-existent class, that's fine, just let them.
|
||||
return { response: {} };
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
});
|
||||
.then(() => req.config.database.schemaCollection())
|
||||
// We've dropped the collection now, so delete the item from _SCHEMA
|
||||
// and clear the _Join collections
|
||||
.then(coll => coll.findAndDeleteSchema(req.params.className))
|
||||
.then(document => {
|
||||
if (document === null) {
|
||||
//tried to delete non-existent class
|
||||
return Promise.resolve();
|
||||
}
|
||||
return removeJoinTables(req.config.database, document);
|
||||
})
|
||||
.then(() => ({ response: {} }));
|
||||
}
|
||||
|
||||
export class SchemasRouter extends PromiseRouter {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
|
||||
import ClassesRouter from './ClassesRouter';
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import rest from '../rest';
|
||||
import Auth from '../Auth';
|
||||
import rest from '../rest';
|
||||
import Auth from '../Auth';
|
||||
|
||||
export class SessionsRouter extends ClassesRouter {
|
||||
handleFind(req) {
|
||||
@@ -36,7 +36,7 @@ export class SessionsRouter extends ClassesRouter {
|
||||
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN,
|
||||
'Session token required.');
|
||||
}
|
||||
return rest.find(req.config, Auth.master(req.config), '_Session', { _session_token: req.info.sessionToken })
|
||||
return rest.find(req.config, Auth.master(req.config), '_Session', { sessionToken: req.info.sessionToken })
|
||||
.then((response) => {
|
||||
if (!response.results || response.results.length == 0) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN,
|
||||
|
||||
@@ -46,7 +46,7 @@ export class UsersRouter extends ClassesRouter {
|
||||
}
|
||||
let sessionToken = req.info.sessionToken;
|
||||
return rest.find(req.config, Auth.master(req.config), '_Session',
|
||||
{ _session_token: sessionToken },
|
||||
{ sessionToken },
|
||||
{ include: 'user' })
|
||||
.then((response) => {
|
||||
if (!response.results ||
|
||||
@@ -139,7 +139,7 @@ export class UsersRouter extends ClassesRouter {
|
||||
let success = {response: {}};
|
||||
if (req.info && req.info.sessionToken) {
|
||||
return rest.find(req.config, Auth.master(req.config), '_Session',
|
||||
{ _session_token: req.info.sessionToken }
|
||||
{ sessionToken: req.info.sessionToken }
|
||||
).then((records) => {
|
||||
if (records.results && records.results.length) {
|
||||
return rest.del(req.config, Auth.master(req.config), '_Session',
|
||||
|
||||
@@ -174,5 +174,10 @@ export default {
|
||||
"verbose": {
|
||||
env: "VERBOSE",
|
||||
help: "Set the logging to verbose"
|
||||
},
|
||||
"revokeSessionOnPasswordReset": {
|
||||
env: "PARSE_SERVER_REVOKE_SESSION_ON_PASSWORD_RESET",
|
||||
help: "When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions.",
|
||||
action: booleanParser
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
var request = require("request");
|
||||
const send = function(method, path, body) {
|
||||
|
||||
|
||||
var Parse = require("parse/node").Parse;
|
||||
|
||||
var options = {
|
||||
@@ -12,7 +12,7 @@ const send = function(method, path, body) {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
if (body) {
|
||||
if (typeof body == "object") {
|
||||
options.body = JSON.stringify(body);
|
||||
@@ -20,7 +20,7 @@ const send = function(method, path, body) {
|
||||
options.body = body;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var promise = new Parse.Promise();
|
||||
request(options, function(err, response, body){
|
||||
if (err) {
|
||||
|
||||
@@ -45,6 +45,10 @@ function handleParseHeaders(req, res, next) {
|
||||
fileViaJSON = true;
|
||||
}
|
||||
|
||||
if (req.body) {
|
||||
delete req.body._RevocableSession;
|
||||
}
|
||||
|
||||
if (req.body &&
|
||||
req.body._ApplicationId &&
|
||||
cache.apps.get(req.body._ApplicationId) &&
|
||||
|
||||
@@ -26,9 +26,9 @@ export default function pushStatusHandler(config) {
|
||||
let data = body.data || {};
|
||||
let payloadString = JSON.stringify(data);
|
||||
let object = {
|
||||
_id: objectId,
|
||||
objectId,
|
||||
createdAt: now,
|
||||
pushTime: now.toISOString(),
|
||||
_created_at: now,
|
||||
query: JSON.stringify(where),
|
||||
payload: payloadString,
|
||||
source: options.source,
|
||||
@@ -38,8 +38,7 @@ export default function pushStatusHandler(config) {
|
||||
numSent: 0,
|
||||
pushHash: md5Hash(payloadString),
|
||||
// lockdown!
|
||||
_wperm: [],
|
||||
_rperm: []
|
||||
ACL: {}
|
||||
}
|
||||
|
||||
return database.create(PUSH_STATUS_COLLECTION, object).then(() => {
|
||||
@@ -54,12 +53,13 @@ export default function pushStatusHandler(config) {
|
||||
logger.verbose('sending push to %d installations', installations.length);
|
||||
return database.update(PUSH_STATUS_COLLECTION,
|
||||
{status:"pending", objectId: objectId},
|
||||
{status: "running"});
|
||||
{status: "running", updatedAt: new Date() });
|
||||
}
|
||||
|
||||
let complete = function(results) {
|
||||
let update = {
|
||||
status: 'succeeded',
|
||||
updatedAt: new Date(),
|
||||
numSent: 0,
|
||||
numFailed: 0,
|
||||
};
|
||||
@@ -67,7 +67,7 @@ export default function pushStatusHandler(config) {
|
||||
results = flatten(results);
|
||||
results.reduce((memo, result) => {
|
||||
// Cannot handle that
|
||||
if (!result.device || !result.device.deviceType) {
|
||||
if (!result || !result.device || !result.device.deviceType) {
|
||||
return memo;
|
||||
}
|
||||
let deviceType = result.device.deviceType;
|
||||
@@ -87,16 +87,17 @@ export default function pushStatusHandler(config) {
|
||||
}, update);
|
||||
}
|
||||
logger.verbose('sent push! %d success, %d failures', update.numSent, update.numFailed);
|
||||
return database.update('_PushStatus', {status:"running", objectId }, update);
|
||||
return database.update(PUSH_STATUS_COLLECTION, {status:"running", objectId }, update);
|
||||
}
|
||||
|
||||
let fail = function(err) {
|
||||
let update = {
|
||||
errorMessage: JSON.stringify(err),
|
||||
status: 'failed'
|
||||
status: 'failed',
|
||||
updatedAt: new Date()
|
||||
}
|
||||
logger.error('error while sending push', err);
|
||||
return database.update('_PushStatus', { objectId }, update);
|
||||
return database.update(PUSH_STATUS_COLLECTION, { objectId }, update);
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
|
||||
+3
-4
@@ -9,7 +9,7 @@
|
||||
|
||||
var Parse = require('parse/node').Parse;
|
||||
import cache from './cache';
|
||||
import Auth from './Auth';
|
||||
import Auth from './Auth';
|
||||
|
||||
var RestQuery = require('./RestQuery');
|
||||
var RestWrite = require('./RestWrite');
|
||||
@@ -52,7 +52,7 @@ function del(config, auth, className, objectId) {
|
||||
inflatedObject = Parse.Object.fromJSON(response.results[0]);
|
||||
// Notify LiveQuery server if possible
|
||||
config.liveQueryController.onAfterDelete(inflatedObject.className, inflatedObject);
|
||||
return triggers.maybeRunTrigger(triggers.Types.beforeDelete, auth, inflatedObject, null, config.applicationId);
|
||||
return triggers.maybeRunTrigger(triggers.Types.beforeDelete, auth, inflatedObject, null, config);
|
||||
}
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Object not found for delete.');
|
||||
@@ -79,7 +79,7 @@ function del(config, auth, className, objectId) {
|
||||
objectId: objectId
|
||||
}, options);
|
||||
}).then(() => {
|
||||
triggers.maybeRunTrigger(triggers.Types.afterDelete, auth, inflatedObject, null, config.applicationId);
|
||||
triggers.maybeRunTrigger(triggers.Types.afterDelete, auth, inflatedObject, null, config);
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
@@ -96,7 +96,6 @@ function create(config, auth, className, restObject) {
|
||||
// Usually, this is just updatedAt.
|
||||
function update(config, auth, className, objectId, restObject) {
|
||||
enforceRoleSecurity('update', className, auth);
|
||||
|
||||
return Promise.resolve().then(() => {
|
||||
if (triggers.getTrigger(className, triggers.Types.beforeSave, config.applicationId) ||
|
||||
triggers.getTrigger(className, triggers.Types.afterSave, config.applicationId) ||
|
||||
|
||||
+13
-10
@@ -16,7 +16,7 @@ const baseStore = function() {
|
||||
base[key] = {};
|
||||
return base;
|
||||
}, {});
|
||||
|
||||
|
||||
return Object.freeze({
|
||||
Functions,
|
||||
Validators,
|
||||
@@ -63,7 +63,7 @@ export function getTrigger(className, triggerType, applicationId) {
|
||||
throw "Missing ApplicationID";
|
||||
}
|
||||
var manager = _triggerStore[applicationId]
|
||||
if (manager
|
||||
if (manager
|
||||
&& manager.Triggers
|
||||
&& manager.Triggers[triggerType]
|
||||
&& manager.Triggers[triggerType][className]) {
|
||||
@@ -92,15 +92,18 @@ export function getValidator(functionName, applicationId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function getRequestObject(triggerType, auth, parseObject, originalParseObject) {
|
||||
export function getRequestObject(triggerType, auth, parseObject, originalParseObject, config) {
|
||||
var request = {
|
||||
triggerName: triggerType,
|
||||
object: parseObject,
|
||||
master: false
|
||||
master: false,
|
||||
log: config.loggerController && config.loggerController.adapter
|
||||
};
|
||||
|
||||
if (originalParseObject) {
|
||||
request.original = originalParseObject;
|
||||
}
|
||||
|
||||
if (!auth) {
|
||||
return request;
|
||||
}
|
||||
@@ -145,19 +148,19 @@ export function getResponseObject(request, resolve, reject) {
|
||||
// Resolves to an object, empty or containing an object key. A beforeSave
|
||||
// trigger will set the object key to the rest format object to save.
|
||||
// originalParseObject is optional, we only need that for befote/afterSave functions
|
||||
export function maybeRunTrigger(triggerType, auth, parseObject, originalParseObject, applicationId) {
|
||||
export function maybeRunTrigger(triggerType, auth, parseObject, originalParseObject, config) {
|
||||
if (!parseObject) {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
return new Promise(function (resolve, reject) {
|
||||
var trigger = getTrigger(parseObject.className, triggerType, applicationId);
|
||||
var trigger = getTrigger(parseObject.className, triggerType, config.applicationId);
|
||||
if (!trigger) return resolve();
|
||||
var request = getRequestObject(triggerType, auth, parseObject, originalParseObject);
|
||||
var request = getRequestObject(triggerType, auth, parseObject, originalParseObject, config);
|
||||
var response = getResponseObject(request, resolve, reject);
|
||||
// Force the current Parse app before the trigger
|
||||
Parse.applicationId = applicationId;
|
||||
Parse.javascriptKey = cache.apps.get(applicationId).javascriptKey || '';
|
||||
Parse.masterKey = cache.apps.get(applicationId).masterKey;
|
||||
Parse.applicationId = config.applicationId;
|
||||
Parse.javascriptKey = config.javascriptKey || '';
|
||||
Parse.masterKey = config.masterKey;
|
||||
trigger(request, response);
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user