Compare commits

..
56 Commits
Author SHA1 Message Date
Drew e60901de6a Version 2.2.8 (#1738) 2016-05-09 16:56:32 -07:00
Fosco Marotto b2f36e192a Merge pull request #1644 from drew-gross/refactor-query-transform
Break dependency of deleteObjectsByQuery on schemaController
2016-05-09 11:21:22 -07:00
andrewchae 10d2988985 Pass dates to cloud code functions (#655) (#1697) 2016-05-09 10:45:57 -07:00
Steven Shipton 37c502bed0 Add config.expireInactiveSession to add support for non-expiring inactive sessions (#1536)
* Create non-expiring session when sessionLength is zero

* Introduce expireInactiveSessions setting
2016-05-06 12:50:45 -07:00
Héctor Ramos b00572de65 Merge pull request #1696 from ParsePlatform/issue-template
Update issue template
2016-05-06 12:46:41 -07:00
Héctor Ramos da5b5ccefa Update ISSUE_TEMPLATE.md 2016-05-06 12:45:47 -07:00
Steven Shipton 7c850fc1ad Fix for #1642 - copy query parameters to request body (#1687)
* Fix for #1642 - copy query parameters to request body

* Add missing request.query to pass test
2016-05-06 12:36:05 -07:00
Héctor Ramos 90df7841ef Update issue template
Additional guidance for what type of details should be included with a bug report.
2016-05-03 12:08:23 -07:00
Héctor Ramos 0b88302bea Link to new migration guide 2016-04-29 21:34:33 -07:00
Héctor Ramos 583bb34995 Link to bug tool for migration issues 2016-04-28 12:56:04 -07:00
Drew Gross 71ae7bee94 better names and comments 2016-04-26 13:08:58 -07:00
Drew Gross 449ca115b1 Break object deletions's dependency on schemaController 2016-04-26 10:23:14 -07:00
Drew Gross d4bd21fcbc remove schema from transformWhere 2016-04-26 10:20:17 -07:00
Drew Gross a926712951 Start using parse format schema in transformQueryKeyValue 2016-04-26 10:12:45 -07:00
Drew Gross 5f564f32f5 simplify a little 2016-04-25 23:39:42 -07:00
Drew Gross 874d10fc74 Check expected type is array from Parse Format Schema 2016-04-25 23:36:25 -07:00
Drew Gross 4371ca164c Pass parse format schema to transformWhere 2016-04-25 22:45:16 -07:00
Drew Gross 608cba9e8c Clearer names in DatabaseController 2016-04-25 22:12:03 -07:00
Drew Gross 5cbf3eb8dd Tidy up db controller 2016-04-25 22:01:50 -07:00
Drew Gross 37953d146b some cleanup 2016-04-25 21:53:57 -07:00
Drew Gross a3179e43fb No need to handle object or reject arrays when transforming queries 2016-04-25 21:13:34 -07:00
Drew Gross 7b431ad064 nits 2016-04-25 20:54:47 -07:00
Aidan Musnitzky 3d7589c1bc Update README.md (#1637) 2016-04-25 20:51:52 -07:00
Drew Gross ee8a3c94d5 No need to transform post-transform keys in mongo adapter 2016-04-25 20:42:19 -07:00
Drew Gross e2a3213351 Remove update logic from query 2016-04-25 19:34:07 -07:00
Drew Gross 5c7b346292 Remove unnecessary logic 2016-04-25 18:48:22 -07:00
Florent Vilmart 0542f860f4 Test case to ensure dashboard.parse.com won't break (#1636)
* Test case to ensure dashboard.parse.com won't break

* Adds volatile classes concept for _PushStatus

* Fixes test
2016-04-25 21:33:11 -04:00
Drew Gross e0a9f19895 Split logic for transforming queries into it's own function 2016-04-25 18:22:20 -07:00
Drew Gross 21e73ac276 Destructuring 2016-04-25 17:21:24 -07:00
Drew 957b5927b1 Ignore _RevoableSession "header" that is sent by JS SDK. Fixes #1548. (#1627) 2016-04-25 15:52:21 -04:00
Drew 234d0093ff Cleanup delete schema (#1604)
* Some cleanup for deleting one schema

* tidyness

* Remove _allCollections as Parse Server doesn't need it.
2016-04-25 14:47:57 -04:00
Drew d14d451028 Move acl adding into parse server (#1601)
* Move writeACL knowledge out of mongoAdapter

* Remove write ACL from mongo adapter

* Remove readACL from Mongo Transform
2016-04-22 21:44:03 -04:00
Drew d33dd68cc5 Add revokeSessionOnPasswordReset option. Closes #1584 (#1597)
* Add revokeSessionOnPasswordReset option

* Fix nits
2016-04-22 18:21:50 -04:00
Drew 0d094767cf Cleanup update (#1590)
* destructuring in DB controller

* deleteObject in db adapter

* Turns out we can't have delete by object ID because of ACLs...

* Fix tests

* destructure acl

* Don't reject with object
2016-04-22 17:05:21 -04:00
Florent Vilmart ab827e3c2f Forces delete resolution (#1595) 2016-04-22 08:46:27 -07:00
Florent Vilmart d49d539a81 _PushStatus uses proper Parse format (#1580)
* removes mongo style props from _PushStatus

* removes log

* Update MongoTransform.js

Removes _id case
2016-04-21 21:36:15 -04:00
Blayne Chard 6e9529f81c Exposes the loggerAdapter as log to Cloud Functions and Triggers (#1565)
This allows access to logging inside cloud code and triggers via
    request.log.info
    request.log.error
2016-04-21 16:20:14 -04:00
Florent Vilmart 2d94a885d2 regression test for #1554 2016-04-20 21:51:48 -04:00
Florent Vilmart b7b57f7ea7 Adds support for Pointer Permissions
* WIP: Initial pointer permissions

* Process Pointer perms when no matching CLP are found

* Additional tests with read lockdown

* Create operation lockdown with pointer permissions,

on parse.com, when an class is locked down with write pointer perm, users can't create objects even if they set their own as the pointer permission key

* Adds test case for multiple write PointerPerms

* Adds validation for pointer permissions when setting

* Adds tests for validating pointer permissions column types

* Adds tests for complex ACL/CLP/PP hierarchy

* Restores power of the master

* Adds validation of borked fields

* Adds complex test for find

* Adds more variations around PointerPermissions tests
2016-04-20 21:51:11 -04:00
Drew b185508508 Remove flaky test
This test depends on external services and is super flaky. Lets kill it.
2016-04-20 19:03:56 -04:00
Drew 7b91ee98ed Fixes #1568 (#1574) 2016-04-20 18:42:18 -04:00
Drew 9776362ab2 Begin isolating object creation code into an externalizable API. (#1569)
* Tidy up transformKeyValue

* Specialize transformKeyValue for object creation

* remove keys that never appear in creation requests

* rename function

* remove local var

* early exit for simple keys

* Refactor create

* Force class creation when creating an object

* Pass parameters to key value transformer

* No need to check for array in this func

* start using Parse Format schema in MongoTransform

* Remove call to getExpectedType

* add tests to ensure client can't see _PushStatus
2016-04-20 13:35:48 -07:00
Florent Vilmart 59b4047de8 Defers the session creation after DB operation (#1561) 2016-04-20 11:57:38 -04:00
Blayne Chard 54b21c2f73 Compress the ACL queries into a single $in query (#1566) 2016-04-20 09:03:44 -04:00
Drew 5d37755d88 Test facebook unlinking (regression test #1532) (#1538) 2016-04-20 00:31:15 -07:00
Florent Vilmart ddd01181be Safely require babel-polyfill (#1558)
(https://github.com/shakacode/bootstrap-loader/pull/29/files)
2016-04-19 22:29:00 -07:00
Drew bf47a0b2b6 Remove acceptor parameter (#1553) 2016-04-19 14:29:05 -07:00
Florent Vilmart 3945e0c680 Update pushStatusHandler.js (#1543)
Fixes error thrown by #1526
2016-04-18 19:30:19 -07:00
Drew 61b62e4e9f Rename Schema => SchemaController (#1542)
* Rename Schema to SchemaController

* Rename Schema => SchemaController

* Move to controllers folder

* Move SchemasController to Controllers folder

* remove ./..
2016-04-18 18:59:57 -07:00
Drew 0708af17d7 Move "No two geopoints" logic into mongo adapter (#1491)
* Move "No two geopoints" logic into mongo adapter

* Semicolon
2016-04-18 17:10:30 -07:00
Drew cecb2a1cb1 Move 'ns not found' into mongo adapter. (#1541) 2016-04-18 17:06:51 -07:00
Drew ab0ea09067 Schema.js cleanup (#1540)
* Tidy up schemas router

* de-duplicate logic for injecting default schemas

* Remove no-op promise

* use hasClass

* Make getAllSchemas part of SchemaController

* Move getOneSchema logic onto schema controller

* remove log
2016-04-18 17:06:00 -07:00
Drew 414ee6740d Regression test for #1489 (#1511) 2016-04-18 19:45:48 -04:00
Fosco Marotto 65bd59d95f Add link to screencast blog in readme. 2016-04-18 13:08:28 -07:00
Drew ac839f75c7 Remove mongo object create format from Parse Server (#1516) 2016-04-18 12:45:48 -04:00
Drew c57c6b33b1 Add link to mandrill adapter. 2016-04-15 14:33:12 -07:00
47 changed files with 2267 additions and 732 deletions
+16 -5
View File
@@ -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.
+17
View File
@@ -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))
+6 -3
View File
@@ -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
View File
@@ -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": {
+61
View File
@@ -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();
});
});
});
+4 -2
View File
@@ -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) {
-11
View File
@@ -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",
+74
View File
@@ -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
View File
@@ -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();
+54
View File
@@ -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();
});
});
});
});
+4 -4
View File
@@ -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
View File
@@ -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();
})
});
});
});
+2 -2
View File
@@ -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
View File
@@ -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");
+58
View File
@@ -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
View File
@@ -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);
})
});
})
});
+697
View File
@@ -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();
});
})
});
+1
View File
@@ -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({}));
+27
View File
@@ -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();
});
});
});
-2
View File
@@ -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
View File
@@ -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
View File
@@ -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();
+28
View File
@@ -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;
}
+212 -115
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+223 -157
View File
@@ -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) {
+3 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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.
+10 -10
View File
@@ -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); });
+13 -5
View File
@@ -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") {
+2 -2
View File
@@ -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: {} } };
+12 -10
View File
@@ -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};
});
+31 -67
View File
@@ -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 {
+3 -3
View File
@@ -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,
+2 -2
View File
@@ -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',
+5
View File
@@ -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
}
};
+3 -3
View File
@@ -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) {
+4
View File
@@ -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) &&
+10 -9
View File
@@ -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
View File
@@ -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
View File
@@ -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);
});
};