mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c4806ba6d | ||
|
|
78646a3e91 | ||
|
|
edf6ab6875 | ||
|
|
142b3beab6 | ||
|
|
b6c19a53ee | ||
|
|
48865c765f | ||
|
|
94178df4d2 | ||
|
|
4a2f7ff2db | ||
|
|
22c790f23c | ||
|
|
0e78c28146 | ||
|
|
23b77f7261 | ||
|
|
f23c0a57ee | ||
|
|
4a5ed1095c | ||
|
|
e788d49af0 | ||
|
|
278027a955 | ||
|
|
f4fe19a55e | ||
|
|
403fb3e062 | ||
|
|
3df10dca88 | ||
|
|
68dbe005b5 | ||
|
|
8c4be63a4c | ||
|
|
e2a5228127 | ||
|
|
151bea25ee | ||
|
|
af55cd1efb |
@@ -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
@@ -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,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() {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
@@ -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
@@ -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 = [];
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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
@@ -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,\
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user