Compare commits

...
23 Commits
Author SHA1 Message Date
Florent Vilmart 5c4806ba6d Changelog 2.2.24 (#2976) 2016-10-31 19:30:20 -04:00
Kulshekhar Kabra 78646a3e91 Enable API related tests for Postgres (#2970) 2016-10-31 12:11:21 -04:00
Kulshekhar Kabra edf6ab6875 Enable query related tests for Postgres (#2969) 2016-10-31 12:10:53 -04:00
Greenkeeper 142b3beab6 chore(package): update lodash to version 4.16.5 (#2972)
https://greenkeeper.io/
2016-10-31 08:19:04 -04:00
Kulshekhar Kabra b6c19a53ee Fix endsWith query test to use the correct query (#2965) 2016-10-31 08:12:58 -04:00
Florent Vilmart 48865c765f Adds ability to update a subscription (#2935)
* Adds ability to update a subscription

* Adds unsubscribe to the RequestSchema, makes sure to not fire unsubscribe to the client when updating

* Fix failing tests

* More extensive tests

* fix annotation
2016-10-31 08:12:11 -04:00
Michael Helvey 94178df4d2 Add functionName to invalid function error (#2963)
Currently in the logs if an invalid function is called the error
message just says "invalid function" which is decidedly unhelpful
when looking through server logs.  Now it will say
"Invalid function: "functionName""
2016-10-30 11:58:34 -04:00
Kulshekhar Kabra 4a2f7ff2db Close client connections on SIGINT/SIGTERM (#2964)
* Close all alive connections on SIGINT/SIGTERM

* Add a comment referencing the node issue.
2016-10-30 11:58:07 -04:00
Kulshekhar Kabra 22c790f23c Wrap postgres class creation in a transaction (#2958) 2016-10-28 20:23:37 -04:00
Florent Vilmart 0e78c28146 Fix PG fails (#2957)
* Better support for arrays of objects

* No transaction in class creation
2016-10-28 16:43:52 -04:00
Florent Vilmart 23b77f7261 Live query pubsub adapter (#2902)
* Moves LiveQuery pub/sub to adapter folder

* Adds ability to provide custom adapter for LiveQuery pubsub

* Adds test for function based adapter

* Pass all options to createSubscriber

* nits
2016-10-28 12:06:35 -04:00
Kulshekhar Kabra f23c0a57ee Fix the error returned when class already exists (#2955)
* Fix the error returned when class already exists

* Wrap the class creation in a transaction
2016-10-28 11:46:19 -04:00
Kulshekhar Kabra 4a5ed1095c Allow the URI for the test DB to be set via env (#2954) 2016-10-28 09:11:07 -04:00
Steven Shipton e788d49af0 Don't require all keys to be configured to enable key checks (#2816) (#2941)
* Add tests. Fail request if any of the 4 optional keys does not match

* Only require one key to be supplied in the request, except when no keys are configured

* Use const over let, var
2016-10-26 16:44:39 -04:00
Florent Vilmart 278027a955 Google auth: try to validate on both sub or user_id (#2922) 2016-10-26 11:44:56 -04:00
Greenkeeper f4fe19a55e chore(package): update request to version 2.76.0 (#2932)
https://greenkeeper.io/
2016-10-25 08:56:27 -04:00
Greenkeeper 403fb3e062 chore(package): update babel-register to version 6.18.0 (#2928)
https://greenkeeper.io/
2016-10-24 22:51:28 -04:00
Greenkeeper 3df10dca88 chore(package): update babel-core to version 6.18.0 (#2925)
https://greenkeeper.io/
2016-10-24 21:35:21 -04:00
Greenkeeper 68dbe005b5 chore(package): update babel-plugin-transform-flow-strip-types to version 6.18.0 (#2926)
https://greenkeeper.io/
2016-10-24 21:35:12 -04:00
Greenkeeper 8c4be63a4c chore(package): update babel-cli to version 6.18.0 (#2924)
https://greenkeeper.io/
2016-10-24 19:26:23 -04:00
Greenkeeper e2a5228127 chore(package): update pg-promise to version 5.3.5 (#2915)
https://greenkeeper.io/
2016-10-24 17:24:46 -04:00
Steven Shipton 151bea25ee Try to retrieve schema from all schemas cache if not found in individual cache (#2912)
* Try to get schema from main schema if not found in single schema

* Add newline

* Add missing return

* Add missing done to tests
2016-10-23 11:59:39 -04:00
Aaron Blondeau af55cd1efb Add role based ACL checks to LiveQuery (#2893)
* Add acl role check to _matchesACL, start adding tests.

* Add tests for ACL role checks in LiveQueryServer.

* Switch to arrow functions, add immutabalized code from @acinader, swap for loop style.
2016-10-21 23:53:31 -04:00
22 changed files with 650 additions and 79 deletions
+17
View File
@@ -1,5 +1,22 @@
## Parse Server Changelog
### 2.2.24
#### New Features
* LiveQuery: Bring your own adapter (#2902), thanks to [Florent Vilmart](https://github.com/flovilmart)
* LiveQuery: Adds "update" operator to update a query subscription (#2935), thanks to [Florent Vilmart](https://github.com/flovilmart)
#### Improvements
* Better Postgres support, thanks to [Kulshekhar Kabra](https://github.com/kulshekhar)
* Logs the function name when failing (#2963), thanks to [Michael Helvey](https://github.com/michaelhelvey)
* CLI: forces closing the connections with SIGINT/SIGTERM (#2964), thanks to [Kulshekhar Kabra](https://github.com/kulshekhar)
* Reduce the number of calls to the _SCHEMA table (#2912), thanks to [Steven Shipton](https://github.com/steven-supersolid)
* LiveQuery: Support for Role ACL's, thanks to [Aaron Blondeau](https://github.com/aaron-blondeau-dose)
#### Bug Fixes
* Better support for checking application and client keys, thanks to [Steven Shipton](https://github.com/steven-supersolid)
* Google OAuth, better support for android and web logins, thanks to [Florent Vilmart](https://github.com/flovilmart)
### 2.2.23
* Run liveQuery server from CLI with a different port, thanks to [Florent Vilmart](https://github.com/flovilmart)
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "parse-server",
"version": "2.2.23",
"version": "2.2.24",
"description": "An express module providing a Parse-compatible API server",
"main": "lib/index.js",
"repository": {
@@ -24,7 +24,7 @@
"deepcopy": "0.6.3",
"express": "4.14.0",
"intersect": "1.0.1",
"lodash": "4.16.4",
"lodash": "4.16.5",
"lru-cache": "4.0.1",
"mime": "1.3.4",
"mongodb": "2.2.10",
@@ -34,9 +34,9 @@
"parse-server-push-adapter": "1.1.0",
"parse-server-s3-adapter": "1.0.5",
"parse-server-simple-mailgun-adapter": "1.0.0",
"pg-promise": "5.3.3",
"pg-promise": "5.3.5",
"redis": "2.6.2",
"request": "2.75.0",
"request": "2.76.0",
"semver": "5.2.0",
"tv4": "1.2.7",
"winston": "2.2.0",
@@ -44,13 +44,13 @@
"ws": "1.1.1"
},
"devDependencies": {
"babel-cli": "6.14.0",
"babel-core": "6.14.0",
"babel-cli": "6.18.0",
"babel-core": "6.18.0",
"babel-plugin-syntax-flow": "6.13.0",
"babel-plugin-transform-flow-strip-types": "6.14.0",
"babel-plugin-transform-flow-strip-types": "6.18.0",
"babel-preset-es2015": "6.14.0",
"babel-preset-stage-0": "6.5.0",
"babel-register": "6.14.0",
"babel-register": "6.18.0",
"bcrypt-nodejs": "0.0.3",
"cross-env": "3.1.3",
"deep-diff": "0.3.4",
+1 -1
View File
@@ -1,4 +1,4 @@
var EventEmitterPubSub = require('../src/LiveQuery/EventEmitterPubSub').EventEmitterPubSub;
var EventEmitterPubSub = require('../src/Adapters/PubSub/EventEmitterPubSub').EventEmitterPubSub;
describe('EventEmitterPubSub', function() {
it('can publish and subscribe', function() {
+54
View File
@@ -17,6 +17,7 @@ describe('middlewares', () => {
return fakeReq.headers[key.toLowerCase()]
}
};
fakeRes = jasmine.createSpyObj('fakeRes', ['end', 'status']);
AppCache.put(fakeReq.body._ApplicationId, {});
});
@@ -35,6 +36,59 @@ describe('middlewares', () => {
});
});
it('should give invalid response when keys are configured but no key supplied', () => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
restAPIKey: 'restAPIKey'
});
middlewares.handleParseHeaders(fakeReq, fakeRes);
expect(fakeRes.status).toHaveBeenCalledWith(403);
});
it('should give invalid response when keys are configured but supplied key is incorrect', () => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
restAPIKey: 'restAPIKey'
});
fakeReq.headers['x-parse-rest-api-key'] = 'wrongKey';
middlewares.handleParseHeaders(fakeReq, fakeRes);
expect(fakeRes.status).toHaveBeenCalledWith(403);
});
it('should give invalid response when keys are configured but different key is supplied', () => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
restAPIKey: 'restAPIKey'
});
fakeReq.headers['x-parse-client-key'] = 'clientKey';
middlewares.handleParseHeaders(fakeReq, fakeRes);
expect(fakeRes.status).toHaveBeenCalledWith(403);
});
it('should succeed when any one of the configured keys supplied', (done) => {
AppCache.put(fakeReq.body._ApplicationId, {
clientKey: 'clientKey',
masterKey: 'masterKey',
restAPIKey: 'restAPIKey'
});
fakeReq.headers['x-parse-rest-api-key'] = 'restAPIKey';
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
expect(fakeRes.status).not.toHaveBeenCalled();
done();
});
});
it('should succeed when no keys are configured and none supplied', (done) => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey'
});
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
expect(fakeRes.status).not.toHaveBeenCalled();
done();
});
});
const BodyParams = {
clientVersion: '_ClientVersion',
installationId: '_InstallationId',
+2 -2
View File
@@ -1129,7 +1129,7 @@ it('ensure that if you try to sign up a user with a unique username and email, b
done();
}, (e) => {
expect(e.code).toEqual(Parse.Error.SCRIPT_FAILED);
expect(e.message).toEqual('Invalid function.');
expect(e.message).toEqual('Invalid function: "somethingThatDoesDefinitelyNotExist"');
done();
});
});
@@ -1306,7 +1306,7 @@ it('ensure that if you try to sign up a user with a unique username and email, b
});
});
it_exclude_dbs(['postgres'])('bans interior keys containing . or $', done => {
it('bans interior keys containing . or $', done => {
new Parse.Object('Obj').save({innerObj: {'key with a $': 'fails'}})
.then(() => {
fail('should not succeed')
+139
View File
@@ -335,6 +335,35 @@ describe('ParseLiveQueryServer', function() {
expect(JSON.stringify(args[1])).toBe(unsubscribeRequest);
});
it('can set update command message handler for a parseWebSocket', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Register mock connect/subscribe/unsubscribe handler for the server
spyOn(parseLiveQueryServer, '_handleUpdateSubscription').and.callThrough();
spyOn(parseLiveQueryServer, '_handleUnsubscribe').and.callThrough();
spyOn(parseLiveQueryServer, '_handleSubscribe').and.callThrough();
// Make mock parseWebsocket
var EventEmitter = require('events');
var parseWebSocket = new EventEmitter();
// Register message handlers for the parseWebSocket
parseLiveQueryServer._onConnect(parseWebSocket);
// Check updateRequest request
var updateRequest = '{"op":"update"}';
// Trigger message event
parseWebSocket.emit('message', updateRequest);
// Make sure _handleUnsubscribe is called
var args = parseLiveQueryServer._handleUpdateSubscription.calls.mostRecent().args;
expect(args[0]).toBe(parseWebSocket);
expect(JSON.stringify(args[1])).toBe(updateRequest);
expect(parseLiveQueryServer._handleUnsubscribe).toHaveBeenCalled();
let unsubArgs = parseLiveQueryServer._handleUnsubscribe.calls.mostRecent().args;
expect(unsubArgs.length).toBe(3);
expect(unsubArgs[2]).toBe(false);
expect(parseLiveQueryServer._handleSubscribe).toHaveBeenCalled();
});
it('can set unknown command message handler for a parseWebSocket', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Make mock parseWebsocket
@@ -834,6 +863,116 @@ describe('ParseLiveQueryServer', function() {
});
});
it('won\'t match ACL that doesn\'t have public read or any roles', function(done){
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
var acl = new Parse.ACL();
acl.setPublicReadAccess(false);
var client = {
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
sessionToken: 'sessionToken'
})
};
var requestId = 0;
parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) {
expect(isMatched).toBe(false);
done();
});
});
it('won\'t match non-public ACL with role when there is no user', function(done){
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
var acl = new Parse.ACL();
acl.setPublicReadAccess(false);
acl.setRoleReadAccess("livequery", true);
var client = {
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
})
};
var requestId = 0;
parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) {
expect(isMatched).toBe(false);
done();
});
});
it('won\'t match ACL with role based read access set to false', function(done){
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
var acl = new Parse.ACL();
acl.setPublicReadAccess(false);
acl.setRoleReadAccess("liveQueryRead", false);
var client = {
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
sessionToken: 'sessionToken'
})
};
var requestId = 0;
spyOn(Parse, "Query").and.callFake(function(){
return {
equalTo(relation, value) {
// Nothing to do here
},
find() {
//Return a role with the name "liveQueryRead" as that is what was set on the ACL
var liveQueryRole = new Parse.Role();
liveQueryRole.set('name', 'liveQueryRead');
return [
liveQueryRole
];
}
}
});
parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) {
expect(isMatched).toBe(false);
done();
});
});
it('will match ACL with role based read access set to true', function(done){
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
var acl = new Parse.ACL();
acl.setPublicReadAccess(false);
acl.setRoleReadAccess("liveQueryRead", true);
var client = {
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
sessionToken: 'sessionToken'
})
};
var requestId = 0;
spyOn(Parse, "Query").and.callFake(function(){
return {
equalTo(relation, value) {
// Nothing to do here
},
find() {
//Return a role with the name "liveQueryRead" as that is what was set on the ACL
var liveQueryRole = new Parse.Role();
liveQueryRole.set('name', 'liveQueryRead');
return [
liveQueryRole
];
}
}
});
parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) {
expect(isMatched).toBe(true);
done();
});
});
it('can validate key when valid key is provided', function() {
var parseLiveQueryServer = new ParseLiveQueryServer({}, {
keyPairs: {
+64 -14
View File
@@ -8,13 +8,13 @@ describe('ParsePubSub', function() {
createPublisher: jasmine.createSpy('createPublisherRedis'),
createSubscriber: jasmine.createSpy('createSubscriberRedis')
};
jasmine.mockLibrary('../src/LiveQuery/RedisPubSub', 'RedisPubSub', mockRedisPubSub);
jasmine.mockLibrary('../src/Adapters/PubSub/RedisPubSub', 'RedisPubSub', mockRedisPubSub);
// Mock EventEmitterPubSub
var mockEventEmitterPubSub = {
createPublisher: jasmine.createSpy('createPublisherEventEmitter'),
createSubscriber: jasmine.createSpy('createSubscriberEventEmitter')
};
jasmine.mockLibrary('../src/LiveQuery/EventEmitterPubSub', 'EventEmitterPubSub', mockEventEmitterPubSub);
jasmine.mockLibrary('../src/Adapters/PubSub/EventEmitterPubSub', 'EventEmitterPubSub', mockEventEmitterPubSub);
done();
});
@@ -23,17 +23,17 @@ describe('ParsePubSub', function() {
redisURL: 'redisURL'
});
var RedisPubSub = require('../src/LiveQuery/RedisPubSub').RedisPubSub;
var EventEmitterPubSub = require('../src/LiveQuery/EventEmitterPubSub').EventEmitterPubSub;
expect(RedisPubSub.createPublisher).toHaveBeenCalledWith('redisURL');
var RedisPubSub = require('../src/Adapters/PubSub/RedisPubSub').RedisPubSub;
var EventEmitterPubSub = require('../src/Adapters/PubSub/EventEmitterPubSub').EventEmitterPubSub;
expect(RedisPubSub.createPublisher).toHaveBeenCalledWith({redisURL: 'redisURL'});
expect(EventEmitterPubSub.createPublisher).not.toHaveBeenCalled();
});
it('can create event emitter publisher', function() {
var publisher = ParsePubSub.createPublisher({});
var RedisPubSub = require('../src/LiveQuery/RedisPubSub').RedisPubSub;
var EventEmitterPubSub = require('../src/LiveQuery/EventEmitterPubSub').EventEmitterPubSub;
var RedisPubSub = require('../src/Adapters/PubSub/RedisPubSub').RedisPubSub;
var EventEmitterPubSub = require('../src/Adapters/PubSub/EventEmitterPubSub').EventEmitterPubSub;
expect(RedisPubSub.createPublisher).not.toHaveBeenCalled();
expect(EventEmitterPubSub.createPublisher).toHaveBeenCalled();
});
@@ -43,23 +43,73 @@ describe('ParsePubSub', function() {
redisURL: 'redisURL'
});
var RedisPubSub = require('../src/LiveQuery/RedisPubSub').RedisPubSub;
var EventEmitterPubSub = require('../src/LiveQuery/EventEmitterPubSub').EventEmitterPubSub;
expect(RedisPubSub.createSubscriber).toHaveBeenCalledWith('redisURL');
var RedisPubSub = require('../src/Adapters/PubSub/RedisPubSub').RedisPubSub;
var EventEmitterPubSub = require('../src/Adapters/PubSub/EventEmitterPubSub').EventEmitterPubSub;
expect(RedisPubSub.createSubscriber).toHaveBeenCalledWith({redisURL: 'redisURL'});
expect(EventEmitterPubSub.createSubscriber).not.toHaveBeenCalled();
});
it('can create event emitter subscriber', function() {
var subscriptionInfos = ParsePubSub.createSubscriber({});
var RedisPubSub = require('../src/LiveQuery/RedisPubSub').RedisPubSub;
var EventEmitterPubSub = require('../src/LiveQuery/EventEmitterPubSub').EventEmitterPubSub;
var RedisPubSub = require('../src/Adapters/PubSub/RedisPubSub').RedisPubSub;
var EventEmitterPubSub = require('../src/Adapters/PubSub/EventEmitterPubSub').EventEmitterPubSub;
expect(RedisPubSub.createSubscriber).not.toHaveBeenCalled();
expect(EventEmitterPubSub.createSubscriber).toHaveBeenCalled();
});
it('can create publisher/sub with custom adapter', function() {
let adapter = {
createPublisher: jasmine.createSpy('createPublisher'),
createSubscriber: jasmine.createSpy('createSubscriber')
}
ParsePubSub.createPublisher({
pubSubAdapter: adapter
});
expect(adapter.createPublisher).toHaveBeenCalled();
ParsePubSub.createSubscriber({
pubSubAdapter: adapter
});
expect(adapter.createSubscriber).toHaveBeenCalled();
var RedisPubSub = require('../src/Adapters/PubSub/RedisPubSub').RedisPubSub;
var EventEmitterPubSub = require('../src/Adapters/PubSub/EventEmitterPubSub').EventEmitterPubSub;
expect(RedisPubSub.createSubscriber).not.toHaveBeenCalled();
expect(EventEmitterPubSub.createSubscriber).not.toHaveBeenCalled();
expect(RedisPubSub.createPublisher).not.toHaveBeenCalled();
expect(EventEmitterPubSub.createPublisher).not.toHaveBeenCalled();
});
it('can create publisher/sub with custom function adapter', function() {
let adapter = {
createPublisher: jasmine.createSpy('createPublisher'),
createSubscriber: jasmine.createSpy('createSubscriber')
}
ParsePubSub.createPublisher({
pubSubAdapter: function() {
return adapter;
}
});
expect(adapter.createPublisher).toHaveBeenCalled();
ParsePubSub.createSubscriber({
pubSubAdapter: function() {
return adapter;
}
});
expect(adapter.createSubscriber).toHaveBeenCalled();
var RedisPubSub = require('../src/Adapters/PubSub/RedisPubSub').RedisPubSub;
var EventEmitterPubSub = require('../src/Adapters/PubSub/EventEmitterPubSub').EventEmitterPubSub;
expect(RedisPubSub.createSubscriber).not.toHaveBeenCalled();
expect(EventEmitterPubSub.createSubscriber).not.toHaveBeenCalled();
expect(RedisPubSub.createPublisher).not.toHaveBeenCalled();
expect(EventEmitterPubSub.createPublisher).not.toHaveBeenCalled();
});
afterEach(function(){
jasmine.restoreLibrary('../src/LiveQuery/RedisPubSub', 'RedisPubSub');
jasmine.restoreLibrary('../src/LiveQuery/EventEmitterPubSub', 'EventEmitterPubSub');
jasmine.restoreLibrary('../src/Adapters/PubSub/RedisPubSub', 'RedisPubSub');
jasmine.restoreLibrary('../src/Adapters/PubSub/EventEmitterPubSub', 'EventEmitterPubSub');
});
});
+11 -7
View File
@@ -1233,7 +1233,7 @@ describe('Parse.Query testing', () => {
query.find(expectError(Parse.Error.INVALID_QUERY, done));
});
it_exclude_dbs(['postgres'])("Use a regex that requires all modifiers", function(done) {
it("Use a regex that requires all modifiers", function(done) {
var thing = new TestObject();
thing.set("myString", "PArSe\nCom");
Parse.Object.saveAll([thing], function() {
@@ -1248,6 +1248,10 @@ describe('Parse.Query testing', () => {
success: function(results) {
equal(results.length, 1);
done();
},
error: function(err) {
jfail(err);
done();
}
});
});
@@ -1271,7 +1275,7 @@ describe('Parse.Query testing', () => {
var someAscii = "\\E' !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTU" +
"VWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~'";
it_exclude_dbs(['postgres'])("contains", function(done) {
it("contains", function(done) {
Parse.Object.saveAll([new TestObject({myString: "zax" + someAscii + "qub"}),
new TestObject({myString: "start" + someAscii}),
new TestObject({myString: someAscii + "end"}),
@@ -1287,7 +1291,7 @@ describe('Parse.Query testing', () => {
});
});
it_exclude_dbs(['postgres'])("startsWith", function(done) {
it("startsWith", function(done) {
Parse.Object.saveAll([new TestObject({myString: "zax" + someAscii + "qub"}),
new TestObject({myString: "start" + someAscii}),
new TestObject({myString: someAscii + "end"}),
@@ -1303,13 +1307,13 @@ describe('Parse.Query testing', () => {
});
});
it_exclude_dbs(['postgres'])("endsWith", function(done) {
it("endsWith", function(done) {
Parse.Object.saveAll([new TestObject({myString: "zax" + someAscii + "qub"}),
new TestObject({myString: "start" + someAscii}),
new TestObject({myString: someAscii + "end"}),
new TestObject({myString: someAscii})], function() {
var query = new Parse.Query(TestObject);
query.startsWith("myString", someAscii);
query.endsWith("myString", someAscii);
query.find({
success: function(results, foo) {
equal(results.length, 2);
@@ -1634,7 +1638,7 @@ describe('Parse.Query testing', () => {
})
});
it_exclude_dbs(['postgres'])('properly nested array of mixed objects with bad ids', (done) => {
it('properly nested array of mixed objects with bad ids', (done) => {
let objects = [];
let total = 0;
while(objects.length != 5) {
@@ -1744,7 +1748,7 @@ describe('Parse.Query testing', () => {
});
});
it_exclude_dbs(['postgres'])("matches query", function(done) {
it("matches query", function(done) {
var ParentObject = Parse.Object.extend("ParentObject");
var ChildObject = Parse.Object.extend("ChildObject");
var objects = [];
+3 -3
View File
@@ -1,4 +1,4 @@
var RedisPubSub = require('../src/LiveQuery/RedisPubSub').RedisPubSub;
var RedisPubSub = require('../src/Adapters/PubSub/RedisPubSub').RedisPubSub;
describe('RedisPubSub', function() {
@@ -10,14 +10,14 @@ describe('RedisPubSub', function() {
});
it('can create publisher', function() {
var publisher = RedisPubSub.createPublisher('redisAddress');
var publisher = RedisPubSub.createPublisher({redisURL: 'redisAddress'});
var redis = require('redis');
expect(redis.createClient).toHaveBeenCalledWith('redisAddress', { no_ready_check: true });
});
it('can create subscriber', function() {
var subscriber = RedisPubSub.createSubscriber('redisAddress');
var subscriber = RedisPubSub.createSubscriber({redisURL: 'redisAddress'});
var redis = require('redis');
expect(redis.createClient).toHaveBeenCalledWith('redisAddress', { no_ready_check: true });
+37
View File
@@ -0,0 +1,37 @@
var CacheController = require('../src/Controllers/CacheController.js').default;
var InMemoryCacheAdapter = require('../src/Adapters/Cache/InMemoryCacheAdapter').default;
var SchemaCache = require('../src/Controllers/SchemaCache').default;
describe('SchemaCache', () => {
var schemaCache;
beforeEach(() => {
var cacheAdapter = new InMemoryCacheAdapter({});
var cacheController = new CacheController(cacheAdapter, 'appId');
schemaCache = new SchemaCache(cacheController);
});
it('can retrieve a single schema after all schemas stored', (done) => {
var allSchemas = [{
className: 'Class1'
}, {
className: 'Class2'
}];
schemaCache.setAllClasses(allSchemas);
schemaCache.getOneSchema('Class2').then((schema) => {
expect(schema).not.toBeNull();
done();
});
});
it('does not return all schemas after a single schema is stored', (done) => {
var schema = {
className: 'Class1'
};
schemaCache.setOneSchema('Class1', schema);
schemaCache.getAllClasses().then((allSchemas) => {
expect(allSchemas).toBeNull();
done();
});
});
});
+1 -1
View File
@@ -41,7 +41,7 @@ let stopDB = () => {};
if (process.env.PARSE_SERVER_TEST_DB === 'postgres') {
databaseAdapter = new PostgresStorageAdapter({
uri: postgresURI,
uri: process.env.PARSE_SERVER_TEST_DATABASE_URI || postgresURI,
collectionPrefix: 'test_',
});
} else {
@@ -1,14 +1,14 @@
import redis from 'redis';
function createPublisher(redisURL: string): any {
function createPublisher({redisURL}): any {
return redis.createClient(redisURL, { no_ready_check: true });
}
function createSubscriber(redisURL: string): any {
function createSubscriber({redisURL}): any {
return redis.createClient(redisURL, { no_ready_check: true });
}
let RedisPubSub = {
const RedisPubSub = {
createPublisher,
createSubscriber
}
@@ -4,6 +4,7 @@ const PostgresRelationDoesNotExistError = '42P01';
const PostgresDuplicateRelationError = '42P07';
const PostgresDuplicateColumnError = '42701';
const PostgresUniqueIndexViolationError = '23505';
const PostgresTransactionAbortedError = '25P02';
const logger = require('../../../logger');
const debug = function(){
@@ -135,6 +136,20 @@ const handleDotFields = (object) => {
return object;
}
const validateKeys = (object) => {
if (typeof object == 'object') {
for (const key in object) {
if (typeof object[key] == 'object') {
validateKeys(object[key]);
}
if(key.includes('$') || key.includes('.')){
throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters");
}
}
}
}
// Returns the list of join tables on a schema
const joinTablesForSchema = (schema) => {
let list = [];
@@ -330,8 +345,14 @@ const buildWhereClause = ({ schema, query, index }) => {
if (opts.indexOf('i') >= 0) {
operator = '~*';
}
if (opts.indexOf('x') >= 0) {
regex = removeWhiteSpace(regex);
}
}
patterns.push(`$${index}:name ${operator} $${index+1}`);
regex = processRegexPattern(regex);
patterns.push(`$${index}:name ${operator} '$${index+1}:raw'`);
values.push(fieldName, regex);
index += 2;
}
@@ -385,8 +406,9 @@ export class PostgresStorageAdapter {
this._client = createClient(uri, databaseOptions);
}
_ensureSchemaCollectionExists() {
return this._client.none('CREATE TABLE "_SCHEMA" ( "className" varChar(120), "schema" jsonb, "isParseClass" bool, PRIMARY KEY ("className") )')
_ensureSchemaCollectionExists(conn) {
conn = conn || this._client;
return conn.none('CREATE TABLE IF NOT EXISTS "_SCHEMA" ( "className" varChar(120), "schema" jsonb, "isParseClass" bool, PRIMARY KEY ("className") )')
.catch(error => {
if (error.code === PostgresDuplicateRelationError || error.code === PostgresUniqueIndexViolationError) {
// Table already exists, must have been created by a different request. Ignore error.
@@ -410,21 +432,30 @@ export class PostgresStorageAdapter {
}
createClass(className, schema) {
return this.createTable(className, schema)
.then(() => this._client.none('INSERT INTO "_SCHEMA" ("className", "schema", "isParseClass") VALUES ($<className>, $<schema>, true)', { className, schema }))
return this._client.tx(t => {
const q1 = this.createTable(className, schema, t);
const q2 = t.none('INSERT INTO "_SCHEMA" ("className", "schema", "isParseClass") VALUES ($<className>, $<schema>, true)', { className, schema });
return t.batch([q1, q2]);
})
.then(() => {
return toParseSchema(schema)
})
.catch((err) => {
if (Array.isArray(err.data) && err.data.length > 1 && err.data[0].result.code === PostgresTransactionAbortedError) {
err = err.data[1].result;
}
if (err.code === PostgresUniqueIndexViolationError && err.detail.includes(className)) {
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`)
throw new Parse.Error(Parse.Error.DUPLICATE_VALUE, `Class ${className} already exists.`)
}
throw err;
})
}
// Just create a table, do not insert in schema
createTable(className, schema) {
createTable(className, schema, conn) {
conn = conn || this._client;
debug('createTable', className, schema);
let valuesArray = [];
let patternsArray = [];
@@ -457,10 +488,10 @@ export class PostgresStorageAdapter {
}
index = index+2;
});
const qs = `CREATE TABLE $1:name (${patternsArray.join(',')})`;
const qs = `CREATE TABLE IF NOT EXISTS $1:name (${patternsArray.join(',')})`;
const values = [className, ...valuesArray];
return this._ensureSchemaCollectionExists()
.then(() => this._client.none(qs, values))
return this._ensureSchemaCollectionExists(conn)
.then(() => conn.none(qs, values))
.catch(error => {
if (error.code === PostgresDuplicateRelationError) {
// Table already exists, must have been created by a different request. Ignore error.
@@ -470,7 +501,7 @@ export class PostgresStorageAdapter {
}).then(() => {
// Create the relation tables
return Promise.all(relations.map((fieldName) => {
return this._client.none('CREATE TABLE IF NOT EXISTS $<joinTable:name> ("relatedId" varChar(120), "owningId" varChar(120), PRIMARY KEY("relatedId", "owningId") )', {joinTable: `_Join:${fieldName}:${className}`});
return conn.none('CREATE TABLE IF NOT EXISTS $<joinTable:name> ("relatedId" varChar(120), "owningId" varChar(120), PRIMARY KEY("relatedId", "owningId") )', {joinTable: `_Join:${fieldName}:${className}`});
}));
});
}
@@ -632,6 +663,8 @@ export class PostgresStorageAdapter {
object = handleDotFields(object);
validateKeys(object);
Object.keys(object).forEach(fieldName => {
var authDataMatch = fieldName.match(/^_auth_data_([a-zA-Z0-9_]+)$/);
if (authDataMatch) {
@@ -891,7 +924,14 @@ export class PostgresStorageAdapter {
if (expectedType === 'text[]') {
updatePatterns.push(`$${index}:name = $${index + 1}::text[]`);
} else {
updatePatterns.push(`$${index}:name = array_to_json($${index + 1}::text[])::jsonb`);
let type = 'text';
for (let elt of fieldValue) {
if (typeof elt == 'object') {
type = 'json';
break;
}
}
updatePatterns.push(`$${index}:name = array_to_json($${index + 1}::${type}[])::jsonb`);
}
values.push(fieldName, fieldValue);
index += 2;
@@ -1113,6 +1153,79 @@ function notImplemented() {
return Promise.reject(new Error('Not implemented yet.'));
}
function removeWhiteSpace(regex) {
if (!regex.endsWith('\n')){
regex += '\n';
}
// remove non escaped comments
return regex.replace(/([^\\])#.*\n/gmi, '$1')
// remove lines starting with a comment
.replace(/^#.*\n/gmi, '')
// remove non escaped whitespace
.replace(/([^\\])\s+/gmi, '$1')
// remove whitespace at the beginning of a line
.replace(/^\s+/, '')
.trim();
}
function processRegexPattern(s) {
if (s && s.startsWith('^')){
// regex for startsWith
return '^' + literalizeRegexPart(s.slice(1));
} else if (s && s.endsWith('$')) {
// regex for endsWith
return literalizeRegexPart(s.slice(0, s.length - 1)) + '$';
}
// regex for contains
return literalizeRegexPart(s);
}
function createLiteralRegex(remaining) {
return remaining.split('').map(c => {
if (c.match(/[0-9a-zA-Z]/) !== null) {
// don't escape alphanumeric characters
return c;
}
// escape everything else (single quotes with single quotes, everything else with a backslash)
return c === `'` ? `''` : `\\${c}`;
}).join('');
}
function literalizeRegexPart(s) {
const matcher1 = /\\Q((?!\\E).*)\\E$/
const result1 = s.match(matcher1);
if(result1 && result1.length > 1 && result1.index > -1){
// process regex that has a beginning and an end specified for the literal text
const prefix = s.substr(0, result1.index);
const remaining = result1[1];
return literalizeRegexPart(prefix) + createLiteralRegex(remaining);
}
// process regex that has a beginning specified for the literal text
const matcher2 = /\\Q((?!\\E).*)$/
const result2 = s.match(matcher2);
if(result2 && result2.length > 1 && result2.index > -1){
const prefix = s.substr(0, result2.index);
const remaining = result2[1];
return literalizeRegexPart(prefix) + createLiteralRegex(remaining);
}
// remove all instances of \Q and \E from the remaining text & escape single quotes
return (
s.replace(/([^\\])(\\E)/, '$1')
.replace(/([^\\])(\\Q)/, '$1')
.replace(/^\\E/, '')
.replace(/^\\Q/, '')
.replace(/([^'])'/, `$1''`)
.replace(/^'([^'])/, `''$1`)
);
}
// Function to set a key on a nested JSON document
const json_object_set_key = 'CREATE OR REPLACE FUNCTION "json_object_set_key"(\
"json" jsonb,\
+15 -1
View File
@@ -50,7 +50,21 @@ export default class SchemaCache {
if (!this.ttl) {
return Promise.resolve(null);
}
return this.cache.get(this.prefix+className);
return this.cache.get(this.prefix+className).then((schema) => {
if (schema) {
return Promise.resolve(schema);
}
return this.cache.get(this.prefix+MAIN_SCHEMA).then((cachedSchemas) => {
cachedSchemas = cachedSchemas || [];
schema = cachedSchemas.find((cachedSchema) => {
return cachedSchema.className === className;
});
if (schema) {
return Promise.resolve(schema);
}
return Promise.resolve(null);
});
});
}
clear() {
+72 -4
View File
@@ -54,9 +54,7 @@ class ParseLiveQueryServer {
);
// Initialize subscriber
this.subscriber = ParsePubSub.createSubscriber({
redisURL: config.redisURL
});
this.subscriber = ParsePubSub.createSubscriber(config);
this.subscriber.subscribe('afterSave');
this.subscriber.subscribe('afterDelete');
// Register message handler for subscriber. When publisher get messages, it will publish message
@@ -259,6 +257,9 @@ class ParseLiveQueryServer {
case 'subscribe':
this._handleSubscribe(parseWebsocket, request);
break;
case 'update':
this._handleUpdateSubscription(parseWebsocket, request);
break;
case 'unsubscribe':
this._handleUnsubscribe(parseWebsocket, request);
break;
@@ -327,6 +328,64 @@ class ParseLiveQueryServer {
if (isSubscriptionSessionTokenMatched) {
return Parse.Promise.as(true);
}
// Check if the user has any roles that match the ACL
return new Parse.Promise((resolve, reject) => {
// Resolve false right away if the acl doesn't have any roles
const acl_has_roles = Object.keys(acl.permissionsById).some(key => key.startsWith("role:"));
if (!acl_has_roles) {
return resolve(false);
}
this.sessionTokenCache.getUserId(subscriptionSessionToken)
.then((userId) => {
// Pass along a null if there is no user id
if (!userId) {
return Parse.Promise.as(null);
}
// Prepare a user object to query for roles
// To eliminate a query for the user, create one locally with the id
var user = new Parse.User();
user.id = userId;
return user;
})
.then((user) => {
// Pass along an empty array (of roles) if no user
if (!user) {
return Parse.Promise.as([]);
}
// Then get the user's roles
var rolesQuery = new Parse.Query(Parse.Role);
rolesQuery.equalTo("users", user);
return rolesQuery.find();
}).
then((roles) => {
// Finally, see if any of the user's roles allow them read access
for (let role of roles) {
if (acl.getRoleReadAccess(role)) {
return resolve(true);
}
}
resolve(false);
})
.catch((error) => {
reject(error);
});
});
}).then((isRoleMatched) => {
if(isRoleMatched) {
return Parse.Promise.as(true);
}
// Check client sessionToken matches ACL
let clientSessionToken = client.sessionToken;
return this.sessionTokenCache.getUserId(clientSessionToken).then((userId) => {
@@ -415,7 +474,12 @@ class ParseLiveQueryServer {
logger.verbose('Current client number: %d', this.clients.size);
}
_handleUnsubscribe(parseWebsocket: any, request: any): any {
_handleUpdateSubscription(parseWebsocket: any, request: any): any {
this._handleUnsubscribe(parseWebsocket, request, false);
this._handleSubscribe(parseWebsocket, request);
}
_handleUnsubscribe(parseWebsocket: any, request: any, notifyClient: bool = true): any {
// If we can not find this client, return error to client
if (!parseWebsocket.hasOwnProperty('clientId')) {
Client.pushError(parseWebsocket, 2, 'Can not find this client, make sure you connect to server before unsubscribing');
@@ -454,6 +518,10 @@ class ParseLiveQueryServer {
if (classSubscriptions.size === 0) {
this.subscriptions.delete(className);
}
if (!notifyClient) {
return;
}
client.pushUnsubscribe(request.requestId);
+20 -6
View File
@@ -1,5 +1,11 @@
import { RedisPubSub } from './RedisPubSub';
import { EventEmitterPubSub } from './EventEmitterPubSub';
import { loadAdapter } from '../Adapters/AdapterLoader';
import {
EventEmitterPubSub
} from '../Adapters/PubSub/EventEmitterPubSub';
import {
RedisPubSub
} from '../Adapters/PubSub/RedisPubSub';
let ParsePubSub = {};
@@ -10,17 +16,25 @@ function useRedis(config: any): boolean {
ParsePubSub.createPublisher = function(config: any): any {
if (useRedis(config)) {
return RedisPubSub.createPublisher(config.redisURL);
return RedisPubSub.createPublisher(config);
} else {
return EventEmitterPubSub.createPublisher();
let adapter = loadAdapter(config.pubSubAdapter, EventEmitterPubSub, config)
if (typeof adapter.createPublisher !== 'function') {
throw 'pubSubAdapter should have createPublisher()';
}
return adapter.createPublisher(config);
}
}
ParsePubSub.createSubscriber = function(config: any): void {
if (useRedis(config)) {
return RedisPubSub.createSubscriber(config.redisURL);
return RedisPubSub.createSubscriber(config);
} else {
return EventEmitterPubSub.createSubscriber();
let adapter = loadAdapter(config.pubSubAdapter, EventEmitterPubSub, config)
if (typeof adapter.createSubscriber !== 'function') {
throw 'pubSubAdapter should have createSubscriber()';
}
return adapter.createSubscriber(config);
}
}
+40 -1
View File
@@ -4,7 +4,7 @@ let general = {
'properties': {
'op': {
'type': 'string',
'enum': ['connect', 'subscribe', 'unsubscribe']
'enum': ['connect', 'subscribe', 'unsubscribe', 'update']
},
},
};
@@ -78,6 +78,44 @@ let subscribe = {
'additionalProperties': false
};
let update = {
'title': 'Update operation schema',
'type': 'object',
'properties': {
'op': 'update',
'requestId': {
'type': 'number'
},
'query': {
'title': 'Query field schema',
'type': 'object',
'properties': {
'className': {
'type': 'string'
},
'where': {
'type': 'object'
},
'fields': {
"type": "array",
"items": {
"type": "string"
},
"minItems": 1,
"uniqueItems": true
}
},
'required': ['where', 'className'],
'additionalProperties': false
},
'sessionToken': {
'type': 'string'
}
},
'required': ['op', 'requestId', 'query'],
'additionalProperties': false
};
let unsubscribe = {
'title': 'Unsubscribe operation schema',
'type': 'object',
@@ -95,6 +133,7 @@ let RequestSchema = {
'general': general,
'connect': connect,
'subscribe': subscribe,
'update': update,
'unsubscribe': unsubscribe
}
+1 -1
View File
@@ -161,7 +161,7 @@ export class FunctionsRouter extends PromiseRouter {
theFunction(request, response);
});
} else {
throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Invalid function.');
throw new Parse.Error(Parse.Error.SCRIPT_FAILED, `Invalid function: "${functionName}"`);
}
}
}
+2 -2
View File
@@ -5,7 +5,7 @@ var Parse = require('parse/node').Parse;
function validateIdToken(id, token) {
return request("tokeninfo?id_token="+token)
.then((response) => {
if (response && response.sub == id) {
if (response && (response.sub == id || response.user_id == id)) {
return;
}
throw new Parse.Error(
@@ -17,7 +17,7 @@ function validateIdToken(id, token) {
function validateAuthToken(id, token) {
return request("tokeninfo?access_token="+token)
.then((response) => {
if (response && response.user_id == id) {
if (response && (response.sub == id || response.user_id == id)) {
return;
}
throw new Parse.Error(
+27
View File
@@ -31,9 +31,13 @@ const help = function(){
function startServer(options, callback) {
const app = express();
const api = new ParseServer(options);
const sockets = {};
app.use(options.mountPath, api);
var server = app.listen(options.port, callback);
server.on('connection', initializeConnections);
if (options.startLiveQueryServer || options.liveQueryServerOptions) {
let liveQueryServer = server;
if (options.liveQueryPort) {
@@ -43,8 +47,31 @@ function startServer(options, callback) {
}
ParseServer.createLiveQueryServer(liveQueryServer, options.liveQueryServerOptions);
}
function initializeConnections(socket) {
/* Currently, express doesn't shut down immediately after receiving SIGINT/SIGTERM if it has client connections that haven't timed out. (This is a known issue with node - https://github.com/nodejs/node/issues/2642)
This function, along with `destroyAliveConnections()`, intend to fix this behavior such that parse server will close all open connections and initiate the shutdown process as soon as it receives a SIGINT/SIGTERM signal. */
const socketId = socket.remoteAddress + ':' + socket.remotePort;
sockets[socketId] = socket;
socket.on('close', () => {
delete sockets[socketId];
});
}
function destroyAliveConnections() {
for (const socketId in sockets) {
try {
sockets[socketId].destroy();
} catch (e) { }
}
}
var handleShutdown = function() {
console.log('Termination signal received. Shutting down.');
destroyAliveConnections();
server.close(function () {
process.exit(0);
});
+8 -13
View File
@@ -120,20 +120,15 @@ export function handleParseHeaders(req, res, next) {
// Client keys are not required in parse-server, but if any have been configured in the server, validate them
// to preserve original behavior.
let keys = ["clientKey", "javascriptKey", "dotNetKey", "restAPIKey"];
const keys = ["clientKey", "javascriptKey", "dotNetKey", "restAPIKey"];
const oneKeyConfigured = keys.some(function(key) {
return req.config[key];
});
const oneKeyMatches = keys.some(function(key){
return req.config[key] && info[key] == req.config[key];
});
// We do it with mismatching keys to support no-keys config
var keyMismatch = keys.reduce(function(mismatch, key){
// check if set in the config and compare
if (req.config[key] && info[key] !== req.config[key]) {
mismatch++;
}
return mismatch;
}, 0);
// All keys mismatch
if (keyMismatch == keys.length) {
if (oneKeyConfigured && !oneKeyMatches) {
return invalidRequest(req, res);
}