Merge pull request #1092 from ParsePlatform/wangmengyan.add_livequery_support
Add LiveQuery to parse server
This commit is contained in:
@@ -29,19 +29,24 @@
|
||||
"deepcopy": "^0.6.1",
|
||||
"express": "^4.13.4",
|
||||
"gcloud": "^0.28.0",
|
||||
"lru-cache": "^4.0.0",
|
||||
"mailgun-js": "^0.7.7",
|
||||
"mime": "^1.3.4",
|
||||
"mongodb": "~2.1.0",
|
||||
"multer": "^1.1.0",
|
||||
"node-gcm": "^0.14.0",
|
||||
"parse": "^1.7.0",
|
||||
"redis": "^2.5.0-1",
|
||||
"request": "^2.65.0",
|
||||
"winston": "^2.1.1"
|
||||
"tv4": "^1.2.7",
|
||||
"winston": "^2.1.1",
|
||||
"ws": "^1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.5.1",
|
||||
"babel-core": "^6.5.1",
|
||||
"babel-istanbul": "^0.6.0",
|
||||
"babel-plugin-syntax-flow": "^6.5.0",
|
||||
"babel-plugin-transform-flow-strip-types": "^6.5.0",
|
||||
"babel-preset-es2015": "^6.5.0",
|
||||
"babel-preset-stage-0": "^6.5.0",
|
||||
|
||||
290
spec/Client.spec.js
Normal file
290
spec/Client.spec.js
Normal file
@@ -0,0 +1,290 @@
|
||||
var Client = require('../src/LiveQuery/Client').Client;
|
||||
var ParseWebSocket = require('../src/LiveQuery/ParseWebSocketServer').ParseWebSocket;
|
||||
|
||||
describe('Client', function() {
|
||||
|
||||
it('can be initialized', function() {
|
||||
var parseWebSocket = new ParseWebSocket({});
|
||||
var client = new Client(1, parseWebSocket);
|
||||
|
||||
expect(client.id).toBe(1);
|
||||
expect(client.parseWebSocket).toBe(parseWebSocket);
|
||||
expect(client.subscriptionInfos.size).toBe(0);
|
||||
});
|
||||
|
||||
it('can push response', function() {
|
||||
var parseWebSocket = {
|
||||
send: jasmine.createSpy('send')
|
||||
};
|
||||
Client.pushResponse(parseWebSocket, 'message');
|
||||
|
||||
expect(parseWebSocket.send).toHaveBeenCalledWith('message');
|
||||
});
|
||||
|
||||
it('can push error', function() {
|
||||
var parseWebSocket = {
|
||||
send: jasmine.createSpy('send')
|
||||
};
|
||||
Client.pushError(parseWebSocket, 1, 'error', true);
|
||||
|
||||
var lastCall = parseWebSocket.send.calls.first();
|
||||
var messageJSON = JSON.parse(lastCall.args[0]);
|
||||
expect(messageJSON.op).toBe('error');
|
||||
expect(messageJSON.error).toBe('error');
|
||||
expect(messageJSON.code).toBe(1);
|
||||
expect(messageJSON.reconnect).toBe(true);
|
||||
});
|
||||
|
||||
it('can add subscription information', function() {
|
||||
var subscription = {};
|
||||
var fields = ['test'];
|
||||
var subscriptionInfo = {
|
||||
subscription: subscription,
|
||||
fields: fields
|
||||
}
|
||||
var client = new Client(1, {});
|
||||
client.addSubscriptionInfo(1, subscriptionInfo);
|
||||
|
||||
expect(client.subscriptionInfos.size).toBe(1);
|
||||
expect(client.subscriptionInfos.get(1)).toBe(subscriptionInfo);
|
||||
});
|
||||
|
||||
it('can get subscription information', function() {
|
||||
var subscription = {};
|
||||
var fields = ['test'];
|
||||
var subscriptionInfo = {
|
||||
subscription: subscription,
|
||||
fields: fields
|
||||
}
|
||||
var client = new Client(1, {});
|
||||
client.addSubscriptionInfo(1, subscriptionInfo);
|
||||
var subscriptionInfoAgain = client.getSubscriptionInfo(1);
|
||||
|
||||
expect(subscriptionInfoAgain).toBe(subscriptionInfo);
|
||||
});
|
||||
|
||||
it('can delete subscription information', function() {
|
||||
var subscription = {};
|
||||
var fields = ['test'];
|
||||
var subscriptionInfo = {
|
||||
subscription: subscription,
|
||||
fields: fields
|
||||
}
|
||||
var client = new Client(1, {});
|
||||
client.addSubscriptionInfo(1, subscriptionInfo);
|
||||
client.deleteSubscriptionInfo(1);
|
||||
|
||||
expect(client.subscriptionInfos.size).toBe(0);
|
||||
});
|
||||
|
||||
|
||||
it('can generate ParseObject JSON with null selected field', function() {
|
||||
var parseObjectJSON = {
|
||||
key : 'value',
|
||||
className: 'test',
|
||||
objectId: 'test',
|
||||
updatedAt: '2015-12-07T21:27:13.746Z',
|
||||
createdAt: '2015-12-07T21:27:13.746Z',
|
||||
ACL: 'test',
|
||||
};
|
||||
var client = new Client(1, {});
|
||||
|
||||
expect(client._toJSONWithFields(parseObjectJSON, null)).toBe(parseObjectJSON);
|
||||
});
|
||||
|
||||
it('can generate ParseObject JSON with undefined selected field', function() {
|
||||
var parseObjectJSON = {
|
||||
key : 'value',
|
||||
className: 'test',
|
||||
objectId: 'test',
|
||||
updatedAt: '2015-12-07T21:27:13.746Z',
|
||||
createdAt: '2015-12-07T21:27:13.746Z',
|
||||
ACL: 'test',
|
||||
};
|
||||
var client = new Client(1, {});
|
||||
|
||||
expect(client._toJSONWithFields(parseObjectJSON, undefined)).toBe(parseObjectJSON);
|
||||
});
|
||||
|
||||
it('can generate ParseObject JSON with selected fields', function() {
|
||||
var parseObjectJSON = {
|
||||
key : 'value',
|
||||
className: 'test',
|
||||
objectId: 'test',
|
||||
updatedAt: '2015-12-07T21:27:13.746Z',
|
||||
createdAt: '2015-12-07T21:27:13.746Z',
|
||||
ACL: 'test',
|
||||
test: 'test'
|
||||
};
|
||||
var client = new Client(1, {});
|
||||
|
||||
expect(client._toJSONWithFields(parseObjectJSON, ['test'])).toEqual({
|
||||
className: 'test',
|
||||
objectId: 'test',
|
||||
updatedAt: '2015-12-07T21:27:13.746Z',
|
||||
createdAt: '2015-12-07T21:27:13.746Z',
|
||||
ACL: 'test',
|
||||
test: 'test'
|
||||
});
|
||||
});
|
||||
|
||||
it('can generate ParseObject JSON with nonexistent selected fields', function() {
|
||||
var parseObjectJSON = {
|
||||
key : 'value',
|
||||
className: 'test',
|
||||
objectId: 'test',
|
||||
updatedAt: '2015-12-07T21:27:13.746Z',
|
||||
createdAt: '2015-12-07T21:27:13.746Z',
|
||||
ACL: 'test',
|
||||
test: 'test'
|
||||
};
|
||||
var client = new Client(1, {});
|
||||
var limitedParseObject = client._toJSONWithFields(parseObjectJSON, ['name']);
|
||||
|
||||
expect(limitedParseObject).toEqual({
|
||||
className: 'test',
|
||||
objectId: 'test',
|
||||
updatedAt: '2015-12-07T21:27:13.746Z',
|
||||
createdAt: '2015-12-07T21:27:13.746Z',
|
||||
ACL: 'test',
|
||||
});
|
||||
expect('name' in limitedParseObject).toBe(false);
|
||||
});
|
||||
|
||||
it('can push connect response', function() {
|
||||
var parseWebSocket = {
|
||||
send: jasmine.createSpy('send')
|
||||
};
|
||||
var client = new Client(1, parseWebSocket);
|
||||
client.pushConnect();
|
||||
|
||||
var lastCall = parseWebSocket.send.calls.first();
|
||||
var messageJSON = JSON.parse(lastCall.args[0]);
|
||||
expect(messageJSON.op).toBe('connected');
|
||||
expect(messageJSON.clientId).toBe(1);
|
||||
});
|
||||
|
||||
it('can push subscribe response', function() {
|
||||
var parseWebSocket = {
|
||||
send: jasmine.createSpy('send')
|
||||
};
|
||||
var client = new Client(1, parseWebSocket);
|
||||
client.pushSubscribe(2);
|
||||
|
||||
var lastCall = parseWebSocket.send.calls.first();
|
||||
var messageJSON = JSON.parse(lastCall.args[0]);
|
||||
expect(messageJSON.op).toBe('subscribed');
|
||||
expect(messageJSON.clientId).toBe(1);
|
||||
expect(messageJSON.requestId).toBe(2);
|
||||
});
|
||||
|
||||
it('can push unsubscribe response', function() {
|
||||
var parseWebSocket = {
|
||||
send: jasmine.createSpy('send')
|
||||
};
|
||||
var client = new Client(1, parseWebSocket);
|
||||
client.pushUnsubscribe(2);
|
||||
|
||||
var lastCall = parseWebSocket.send.calls.first();
|
||||
var messageJSON = JSON.parse(lastCall.args[0]);
|
||||
expect(messageJSON.op).toBe('unsubscribed');
|
||||
expect(messageJSON.clientId).toBe(1);
|
||||
expect(messageJSON.requestId).toBe(2);
|
||||
});
|
||||
|
||||
it('can push create response', function() {
|
||||
var parseObjectJSON = {
|
||||
key : 'value',
|
||||
className: 'test',
|
||||
objectId: 'test',
|
||||
updatedAt: '2015-12-07T21:27:13.746Z',
|
||||
createdAt: '2015-12-07T21:27:13.746Z',
|
||||
ACL: 'test',
|
||||
test: 'test'
|
||||
};
|
||||
var parseWebSocket = {
|
||||
send: jasmine.createSpy('send')
|
||||
};
|
||||
var client = new Client(1, parseWebSocket);
|
||||
client.pushCreate(2, parseObjectJSON);
|
||||
|
||||
var lastCall = parseWebSocket.send.calls.first();
|
||||
var messageJSON = JSON.parse(lastCall.args[0]);
|
||||
expect(messageJSON.op).toBe('create');
|
||||
expect(messageJSON.clientId).toBe(1);
|
||||
expect(messageJSON.requestId).toBe(2);
|
||||
expect(messageJSON.object).toEqual(parseObjectJSON);
|
||||
});
|
||||
|
||||
it('can push enter response', function() {
|
||||
var parseObjectJSON = {
|
||||
key : 'value',
|
||||
className: 'test',
|
||||
objectId: 'test',
|
||||
updatedAt: '2015-12-07T21:27:13.746Z',
|
||||
createdAt: '2015-12-07T21:27:13.746Z',
|
||||
ACL: 'test',
|
||||
test: 'test'
|
||||
};
|
||||
var parseWebSocket = {
|
||||
send: jasmine.createSpy('send')
|
||||
};
|
||||
var client = new Client(1, parseWebSocket);
|
||||
client.pushEnter(2, parseObjectJSON);
|
||||
|
||||
var lastCall = parseWebSocket.send.calls.first();
|
||||
var messageJSON = JSON.parse(lastCall.args[0]);
|
||||
expect(messageJSON.op).toBe('enter');
|
||||
expect(messageJSON.clientId).toBe(1);
|
||||
expect(messageJSON.requestId).toBe(2);
|
||||
expect(messageJSON.object).toEqual(parseObjectJSON);
|
||||
});
|
||||
|
||||
it('can push update response', function() {
|
||||
var parseObjectJSON = {
|
||||
key : 'value',
|
||||
className: 'test',
|
||||
objectId: 'test',
|
||||
updatedAt: '2015-12-07T21:27:13.746Z',
|
||||
createdAt: '2015-12-07T21:27:13.746Z',
|
||||
ACL: 'test',
|
||||
test: 'test'
|
||||
};
|
||||
var parseWebSocket = {
|
||||
send: jasmine.createSpy('send')
|
||||
};
|
||||
var client = new Client(1, parseWebSocket);
|
||||
client.pushUpdate(2, parseObjectJSON);
|
||||
|
||||
var lastCall = parseWebSocket.send.calls.first();
|
||||
var messageJSON = JSON.parse(lastCall.args[0]);
|
||||
expect(messageJSON.op).toBe('update');
|
||||
expect(messageJSON.clientId).toBe(1);
|
||||
expect(messageJSON.requestId).toBe(2);
|
||||
expect(messageJSON.object).toEqual(parseObjectJSON);
|
||||
});
|
||||
|
||||
it('can push leave response', function() {
|
||||
var parseObjectJSON = {
|
||||
key : 'value',
|
||||
className: 'test',
|
||||
objectId: 'test',
|
||||
updatedAt: '2015-12-07T21:27:13.746Z',
|
||||
createdAt: '2015-12-07T21:27:13.746Z',
|
||||
ACL: 'test',
|
||||
test: 'test'
|
||||
};
|
||||
var parseWebSocket = {
|
||||
send: jasmine.createSpy('send')
|
||||
};
|
||||
var client = new Client(1, parseWebSocket);
|
||||
client.pushLeave(2, parseObjectJSON);
|
||||
|
||||
var lastCall = parseWebSocket.send.calls.first();
|
||||
var messageJSON = JSON.parse(lastCall.args[0]);
|
||||
expect(messageJSON.op).toBe('leave');
|
||||
expect(messageJSON.clientId).toBe(1);
|
||||
expect(messageJSON.requestId).toBe(2);
|
||||
expect(messageJSON.object).toEqual(parseObjectJSON);
|
||||
});
|
||||
});
|
||||
44
spec/EventEmitterPubSub.spec.js
Normal file
44
spec/EventEmitterPubSub.spec.js
Normal file
@@ -0,0 +1,44 @@
|
||||
var EventEmitterPubSub = require('../src/LiveQuery/EventEmitterPubSub').EventEmitterPubSub;
|
||||
|
||||
describe('EventEmitterPubSub', function() {
|
||||
|
||||
it('can publish and subscribe', function() {
|
||||
var publisher = EventEmitterPubSub.createPublisher();
|
||||
var subscriber = EventEmitterPubSub.createSubscriber();
|
||||
subscriber.subscribe('testChannel');
|
||||
// Register mock checked for subscriber
|
||||
var isChecked = false;
|
||||
subscriber.on('message', function(channel, message) {
|
||||
isChecked = true;
|
||||
expect(channel).toBe('testChannel');
|
||||
expect(message).toBe('testMessage');
|
||||
});
|
||||
|
||||
publisher.publish('testChannel', 'testMessage');
|
||||
// Make sure the callback is checked
|
||||
expect(isChecked).toBe(true);
|
||||
});
|
||||
|
||||
it('can unsubscribe', function() {
|
||||
var publisher = EventEmitterPubSub.createPublisher();
|
||||
var subscriber = EventEmitterPubSub.createSubscriber();
|
||||
subscriber.subscribe('testChannel');
|
||||
subscriber.unsubscribe('testChannel');
|
||||
// Register mock checked for subscriber
|
||||
var isCalled = false;
|
||||
subscriber.on('message', function(channel, message) {
|
||||
isCalled = true;
|
||||
});
|
||||
|
||||
publisher.publish('testChannel', 'testMessage');
|
||||
// Make sure the callback is not called
|
||||
expect(isCalled).toBe(false);
|
||||
});
|
||||
|
||||
it('can unsubscribe not subscribing channel', function() {
|
||||
var subscriber = EventEmitterPubSub.createSubscriber();
|
||||
|
||||
// Make sure subscriber does not throw exception
|
||||
subscriber.unsubscribe('testChannel');
|
||||
});
|
||||
});
|
||||
70
spec/ParseCloudCodePublisher.spec.js
Normal file
70
spec/ParseCloudCodePublisher.spec.js
Normal file
@@ -0,0 +1,70 @@
|
||||
var ParseCloudCodePublisher = require('../src/LiveQuery/ParseCloudCodePublisher').ParseCloudCodePublisher;
|
||||
var Parse = require('parse/node');
|
||||
|
||||
describe('ParseCloudCodePublisher', function() {
|
||||
|
||||
beforeEach(function(done) {
|
||||
// Mock ParsePubSub
|
||||
var mockParsePubSub = {
|
||||
createPublisher: jasmine.createSpy('publish').and.returnValue({
|
||||
publish: jasmine.createSpy('publish'),
|
||||
on: jasmine.createSpy('on')
|
||||
}),
|
||||
createSubscriber: jasmine.createSpy('publish').and.returnValue({
|
||||
subscribe: jasmine.createSpy('subscribe'),
|
||||
on: jasmine.createSpy('on')
|
||||
})
|
||||
};
|
||||
jasmine.mockLibrary('../src/LiveQuery/ParsePubSub', 'ParsePubSub', mockParsePubSub);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can initialize', function() {
|
||||
var config = {}
|
||||
var publisher = new ParseCloudCodePublisher(config);
|
||||
|
||||
var ParsePubSub = require('../src/LiveQuery/ParsePubSub').ParsePubSub;
|
||||
expect(ParsePubSub.createPublisher).toHaveBeenCalledWith(config);
|
||||
});
|
||||
|
||||
it('can handle cloud code afterSave request', function() {
|
||||
var publisher = new ParseCloudCodePublisher({});
|
||||
publisher._onCloudCodeMessage = jasmine.createSpy('onCloudCodeMessage');
|
||||
var request = {};
|
||||
publisher.onCloudCodeAfterSave(request);
|
||||
|
||||
expect(publisher._onCloudCodeMessage).toHaveBeenCalledWith('afterSave', request);
|
||||
});
|
||||
|
||||
it('can handle cloud code afterDelete request', function() {
|
||||
var publisher = new ParseCloudCodePublisher({});
|
||||
publisher._onCloudCodeMessage = jasmine.createSpy('onCloudCodeMessage');
|
||||
var request = {};
|
||||
publisher.onCloudCodeAfterDelete(request);
|
||||
|
||||
expect(publisher._onCloudCodeMessage).toHaveBeenCalledWith('afterDelete', request);
|
||||
});
|
||||
|
||||
it('can handle cloud code request', function() {
|
||||
var publisher = new ParseCloudCodePublisher({});
|
||||
var currentParseObject = new Parse.Object('Test');
|
||||
currentParseObject.set('key', 'value');
|
||||
var originalParseObject = new Parse.Object('Test');
|
||||
originalParseObject.set('key', 'originalValue');
|
||||
var request = {
|
||||
object: currentParseObject,
|
||||
original: originalParseObject
|
||||
};
|
||||
publisher._onCloudCodeMessage('afterSave', request);
|
||||
|
||||
var args = publisher.parsePublisher.publish.calls.mostRecent().args;
|
||||
expect(args[0]).toBe('afterSave');
|
||||
var message = JSON.parse(args[1]);
|
||||
expect(message.currentParseObject).toEqual(request.object._toFullJSON());
|
||||
expect(message.originalParseObject).toEqual(request.original._toFullJSON());
|
||||
});
|
||||
|
||||
afterEach(function(){
|
||||
jasmine.restoreLibrary('../src/LiveQuery/ParsePubSub', 'ParsePubSub');
|
||||
});
|
||||
});
|
||||
962
spec/ParseLiveQueryServer.spec.js
Normal file
962
spec/ParseLiveQueryServer.spec.js
Normal file
@@ -0,0 +1,962 @@
|
||||
var Parse = require('parse/node');
|
||||
var ParseLiveQueryServer = require('../src/LiveQuery/ParseLiveQueryServer').ParseLiveQueryServer;
|
||||
|
||||
// Global mock info
|
||||
var queryHashValue = 'hash';
|
||||
var testUserId = 'userId';
|
||||
var testClassName = 'TestObject';
|
||||
|
||||
describe('ParseLiveQueryServer', function() {
|
||||
|
||||
beforeEach(function(done) {
|
||||
// Mock ParseWebSocketServer
|
||||
var mockParseWebSocketServer = jasmine.createSpy('ParseWebSocketServer');
|
||||
jasmine.mockLibrary('../src/LiveQuery/ParseWebSocketServer', 'ParseWebSocketServer', mockParseWebSocketServer);
|
||||
// Mock Client
|
||||
var mockClient = function() {
|
||||
this.pushConnect = jasmine.createSpy('pushConnect');
|
||||
this.pushSubscribe = jasmine.createSpy('pushSubscribe');
|
||||
this.pushUnsubscribe = jasmine.createSpy('pushUnsubscribe');
|
||||
this.pushDelete = jasmine.createSpy('pushDelete');
|
||||
this.pushCreate = jasmine.createSpy('pushCreate');
|
||||
this.pushEnter = jasmine.createSpy('pushEnter');
|
||||
this.pushUpdate = jasmine.createSpy('pushUpdate');
|
||||
this.pushLeave = jasmine.createSpy('pushLeave');
|
||||
this.addSubscriptionInfo = jasmine.createSpy('addSubscriptionInfo');
|
||||
this.getSubscriptionInfo = jasmine.createSpy('getSubscriptionInfo');
|
||||
this.deleteSubscriptionInfo = jasmine.createSpy('deleteSubscriptionInfo');
|
||||
}
|
||||
mockClient.pushError = jasmine.createSpy('pushError');
|
||||
jasmine.mockLibrary('../src/LiveQuery/Client', 'Client', mockClient);
|
||||
// Mock Subscription
|
||||
var mockSubscriotion = function() {
|
||||
this.addClientSubscription = jasmine.createSpy('addClientSubscription');
|
||||
this.deleteClientSubscription = jasmine.createSpy('deleteClientSubscription');
|
||||
}
|
||||
jasmine.mockLibrary('../src/LiveQuery/Subscription', 'Subscription', mockSubscriotion);
|
||||
// Mock queryHash
|
||||
var mockQueryHash = jasmine.createSpy('matchesQuery').and.returnValue(queryHashValue);
|
||||
jasmine.mockLibrary('../src/LiveQuery/QueryTools', 'queryHash', mockQueryHash);
|
||||
// Mock matchesQuery
|
||||
var mockMatchesQuery = jasmine.createSpy('matchesQuery').and.returnValue(true);
|
||||
jasmine.mockLibrary('../src/LiveQuery/QueryTools', 'matchesQuery', mockMatchesQuery);
|
||||
// Mock tv4
|
||||
var mockValidate = function() {
|
||||
return true;
|
||||
}
|
||||
jasmine.mockLibrary('tv4', 'validate', mockValidate);
|
||||
// Mock ParsePubSub
|
||||
var mockParsePubSub = {
|
||||
createPublisher: function() {
|
||||
return {
|
||||
publish: jasmine.createSpy('publish'),
|
||||
on: jasmine.createSpy('on')
|
||||
}
|
||||
},
|
||||
createSubscriber: function() {
|
||||
return {
|
||||
subscribe: jasmine.createSpy('subscribe'),
|
||||
on: jasmine.createSpy('on')
|
||||
}
|
||||
}
|
||||
};
|
||||
jasmine.mockLibrary('../src/LiveQuery/ParsePubSub', 'ParsePubSub', mockParsePubSub);
|
||||
// Make mock SessionTokenCache
|
||||
var mockSessionTokenCache = function(){
|
||||
this.getUserId = function(sessionToken){
|
||||
if (typeof sessionToken === 'undefined') {
|
||||
return Parse.Promise.as(undefined);
|
||||
}
|
||||
if (sessionToken === null) {
|
||||
return Parse.Promise.error();
|
||||
}
|
||||
return Parse.Promise.as(testUserId);
|
||||
};
|
||||
};
|
||||
jasmine.mockLibrary('../src/LiveQuery/SessionTokenCache', 'SessionTokenCache', mockSessionTokenCache);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can be initialized', function() {
|
||||
var httpServer = {};
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, httpServer);
|
||||
|
||||
expect(parseLiveQueryServer.clientId).toBe(0);
|
||||
expect(parseLiveQueryServer.clients.size).toBe(0);
|
||||
expect(parseLiveQueryServer.subscriptions.size).toBe(0);
|
||||
});
|
||||
|
||||
it('can handle connect command', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
var parseWebSocket = {
|
||||
clientId: -1
|
||||
};
|
||||
parseLiveQueryServer._validateKeys = jasmine.createSpy('validateKeys').and.returnValue(true);
|
||||
parseLiveQueryServer._handleConnect(parseWebSocket);
|
||||
|
||||
expect(parseLiveQueryServer.clientId).toBe(1);
|
||||
expect(parseWebSocket.clientId).toBe(0);
|
||||
var client = parseLiveQueryServer.clients.get(0);
|
||||
expect(client).not.toBeNull();
|
||||
// Make sure we send connect response to the client
|
||||
expect(client.pushConnect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can handle subscribe command without clientId', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
var incompleteParseConn = {
|
||||
};
|
||||
parseLiveQueryServer._handleSubscribe(incompleteParseConn, {});
|
||||
|
||||
var Client = require('../src/LiveQuery/Client').Client;
|
||||
expect(Client.pushError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can handle subscribe command with new query', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Add mock client
|
||||
var clientId = 1;
|
||||
var client = addMockClient(parseLiveQueryServer, clientId);
|
||||
// Handle mock subscription
|
||||
var parseWebSocket = {
|
||||
clientId: clientId
|
||||
};
|
||||
var query = {
|
||||
className: 'test',
|
||||
where: {
|
||||
key: 'value'
|
||||
},
|
||||
fields: [ 'test' ]
|
||||
}
|
||||
var requestId = 2;
|
||||
var request = {
|
||||
query: query,
|
||||
requestId: requestId,
|
||||
sessionToken: 'sessionToken'
|
||||
}
|
||||
parseLiveQueryServer._handleSubscribe(parseWebSocket, request);
|
||||
|
||||
// Make sure we add the subscription to the server
|
||||
var subscriptions = parseLiveQueryServer.subscriptions;
|
||||
expect(subscriptions.size).toBe(1);
|
||||
expect(subscriptions.get(query.className)).not.toBeNull();
|
||||
var classSubscriptions = subscriptions.get(query.className);
|
||||
expect(classSubscriptions.size).toBe(1);
|
||||
expect(classSubscriptions.get('hash')).not.toBeNull();
|
||||
// TODO(check subscription constructor to verify we pass the right argument)
|
||||
// Make sure we add clientInfo to the subscription
|
||||
var subscription = classSubscriptions.get('hash');
|
||||
expect(subscription.addClientSubscription).toHaveBeenCalledWith(clientId, requestId);
|
||||
// Make sure we add subscriptionInfo to the client
|
||||
var args = client.addSubscriptionInfo.calls.first().args;
|
||||
expect(args[0]).toBe(requestId);
|
||||
expect(args[1].fields).toBe(query.fields);
|
||||
expect(args[1].sessionToken).toBe(request.sessionToken);
|
||||
// Make sure we send subscribe response to the client
|
||||
expect(client.pushSubscribe).toHaveBeenCalledWith(requestId);
|
||||
});
|
||||
|
||||
it('can handle subscribe command with existing query', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Add two mock clients
|
||||
var clientId = 1;
|
||||
var client = addMockClient(parseLiveQueryServer, clientId);
|
||||
var clientIdAgain = 2;
|
||||
var clientAgain = addMockClient(parseLiveQueryServer, clientIdAgain);
|
||||
// Add subscription for mock client 1
|
||||
var parseWebSocket = {
|
||||
clientId: clientId
|
||||
};
|
||||
var requestId = 2;
|
||||
var query = {
|
||||
className: 'test',
|
||||
where: {
|
||||
key: 'value'
|
||||
},
|
||||
fields: [ 'test' ]
|
||||
}
|
||||
addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket, query);
|
||||
// Add subscription for mock client 2
|
||||
var parseWebSocketAgain = {
|
||||
clientId: clientIdAgain
|
||||
};
|
||||
var queryAgain = {
|
||||
className: 'test',
|
||||
where: {
|
||||
key: 'value'
|
||||
},
|
||||
fields: [ 'testAgain' ]
|
||||
}
|
||||
var requestIdAgain = 1;
|
||||
addMockSubscription(parseLiveQueryServer, clientIdAgain, requestIdAgain, parseWebSocketAgain, queryAgain);
|
||||
|
||||
// Make sure we only have one subscription
|
||||
var subscriptions = parseLiveQueryServer.subscriptions;
|
||||
expect(subscriptions.size).toBe(1);
|
||||
expect(subscriptions.get(query.className)).not.toBeNull();
|
||||
var classSubscriptions = subscriptions.get(query.className);
|
||||
expect(classSubscriptions.size).toBe(1);
|
||||
expect(classSubscriptions.get('hash')).not.toBeNull();
|
||||
// Make sure we add clientInfo to the subscription
|
||||
var subscription = classSubscriptions.get('hash');
|
||||
// Make sure client 2 info has been added
|
||||
var args = subscription.addClientSubscription.calls.mostRecent().args;
|
||||
expect(args).toEqual([clientIdAgain, requestIdAgain]);
|
||||
// Make sure we add subscriptionInfo to the client 2
|
||||
args = clientAgain.addSubscriptionInfo.calls.mostRecent().args;
|
||||
expect(args[0]).toBe(requestIdAgain);
|
||||
expect(args[1].fields).toBe(queryAgain.fields);
|
||||
});
|
||||
|
||||
it('can handle unsubscribe command without clientId', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
var incompleteParseConn = {
|
||||
};
|
||||
parseLiveQueryServer._handleUnsubscribe(incompleteParseConn, {});
|
||||
|
||||
var Client = require('../src/LiveQuery/Client').Client;
|
||||
expect(Client.pushError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can handle unsubscribe command without not existed client', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
var parseWebSocket = {
|
||||
clientId: 1
|
||||
};
|
||||
parseLiveQueryServer._handleUnsubscribe(parseWebSocket, {});
|
||||
|
||||
var Client = require('../src/LiveQuery/Client').Client;
|
||||
expect(Client.pushError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can handle unsubscribe command without not existed query', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Add mock client
|
||||
var clientId = 1;
|
||||
var client = addMockClient(parseLiveQueryServer, clientId);
|
||||
// Handle unsubscribe command
|
||||
var parseWebSocket = {
|
||||
clientId: 1
|
||||
};
|
||||
parseLiveQueryServer._handleUnsubscribe(parseWebSocket, {});
|
||||
|
||||
var Client = require('../src/LiveQuery/Client').Client;
|
||||
expect(Client.pushError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can handle unsubscribe command', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Add mock client
|
||||
var clientId = 1;
|
||||
var client = addMockClient(parseLiveQueryServer, clientId);
|
||||
// Add subscription for mock client
|
||||
var parseWebSocket = {
|
||||
clientId: 1
|
||||
};
|
||||
var requestId = 2;
|
||||
var subscription = addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket);
|
||||
// Mock client.getSubscriptionInfo
|
||||
var subscriptionInfo = client.addSubscriptionInfo.calls.mostRecent().args[1];
|
||||
client.getSubscriptionInfo = function() {
|
||||
return subscriptionInfo;
|
||||
};
|
||||
// Handle unsubscribe command
|
||||
var requestAgain = {
|
||||
requestId: requestId
|
||||
};
|
||||
parseLiveQueryServer._handleUnsubscribe(parseWebSocket, requestAgain);
|
||||
|
||||
// Make sure we delete subscription from client
|
||||
expect(client.deleteSubscriptionInfo).toHaveBeenCalledWith(requestId);
|
||||
// Make sure we delete client from subscription
|
||||
expect(subscription.deleteClientSubscription).toHaveBeenCalledWith(clientId, requestId);
|
||||
// Make sure we clear subscription in the server
|
||||
var subscriptions = parseLiveQueryServer.subscriptions;
|
||||
expect(subscriptions.size).toBe(0);
|
||||
});
|
||||
|
||||
it('can set connect command message handler for a parseWebSocket', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Register mock connect/subscribe/unsubscribe handler for the server
|
||||
parseLiveQueryServer._handleConnect = jasmine.createSpy('_handleSubscribe');
|
||||
// Make mock parseWebsocket
|
||||
var EventEmitter = require('events');
|
||||
var parseWebSocket = new EventEmitter();
|
||||
// Register message handlers for the parseWebSocket
|
||||
parseLiveQueryServer._onConnect(parseWebSocket);
|
||||
|
||||
// Check connect request
|
||||
var connectRequest = {
|
||||
op: 'connect'
|
||||
};
|
||||
// Trigger message event
|
||||
parseWebSocket.emit('message', connectRequest);
|
||||
// Make sure _handleConnect is called
|
||||
var args = parseLiveQueryServer._handleConnect.calls.mostRecent().args;
|
||||
expect(args[0]).toBe(parseWebSocket);
|
||||
});
|
||||
|
||||
it('can set subscribe command message handler for a parseWebSocket', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Register mock connect/subscribe/unsubscribe handler for the server
|
||||
parseLiveQueryServer._handleSubscribe = jasmine.createSpy('_handleSubscribe');
|
||||
// Make mock parseWebsocket
|
||||
var EventEmitter = require('events');
|
||||
var parseWebSocket = new EventEmitter();
|
||||
// Register message handlers for the parseWebSocket
|
||||
parseLiveQueryServer._onConnect(parseWebSocket);
|
||||
|
||||
// Check subscribe request
|
||||
var subscribeRequest = '{"op":"subscribe"}';
|
||||
// Trigger message event
|
||||
parseWebSocket.emit('message', subscribeRequest);
|
||||
// Make sure _handleSubscribe is called
|
||||
var args = parseLiveQueryServer._handleSubscribe.calls.mostRecent().args;
|
||||
expect(args[0]).toBe(parseWebSocket);
|
||||
expect(JSON.stringify(args[1])).toBe(subscribeRequest);
|
||||
});
|
||||
|
||||
it('can set unsubscribe command message handler for a parseWebSocket', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Register mock connect/subscribe/unsubscribe handler for the server
|
||||
parseLiveQueryServer._handleUnsubscribe = jasmine.createSpy('_handleSubscribe');
|
||||
// Make mock parseWebsocket
|
||||
var EventEmitter = require('events');
|
||||
var parseWebSocket = new EventEmitter();
|
||||
// Register message handlers for the parseWebSocket
|
||||
parseLiveQueryServer._onConnect(parseWebSocket);
|
||||
|
||||
// Check unsubscribe request
|
||||
var unsubscribeRequest = '{"op":"unsubscribe"}';
|
||||
// Trigger message event
|
||||
parseWebSocket.emit('message', unsubscribeRequest);
|
||||
// Make sure _handleUnsubscribe is called
|
||||
var args = parseLiveQueryServer._handleUnsubscribe.calls.mostRecent().args;
|
||||
expect(args[0]).toBe(parseWebSocket);
|
||||
expect(JSON.stringify(args[1])).toBe(unsubscribeRequest);
|
||||
});
|
||||
|
||||
it('can set unknown command message handler for a parseWebSocket', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Make mock parseWebsocket
|
||||
var EventEmitter = require('events');
|
||||
var parseWebSocket = new EventEmitter();
|
||||
// Register message handlers for the parseWebSocket
|
||||
parseLiveQueryServer._onConnect(parseWebSocket);
|
||||
|
||||
// Check unknown request
|
||||
var unknownRequest = '{"op":"unknown"}';
|
||||
// Trigger message event
|
||||
parseWebSocket.emit('message', unknownRequest);
|
||||
var Client = require('../src/LiveQuery/Client').Client;
|
||||
expect(Client.pushError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can set disconnect command message handler for a parseWebSocket which has not registered to the server', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
var EventEmitter = require('events');
|
||||
var parseWebSocket = new EventEmitter();
|
||||
parseWebSocket.clientId = 1;
|
||||
// Register message handlers for the parseWebSocket
|
||||
parseLiveQueryServer._onConnect(parseWebSocket);
|
||||
|
||||
// Make sure we do not crash
|
||||
// Trigger disconnect event
|
||||
parseWebSocket.emit('disconnect');
|
||||
});
|
||||
|
||||
// TODO: Test server can set disconnect command message handler for a parseWebSocket
|
||||
|
||||
it('has no subscription and can handle object delete command', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Make deletedParseObject
|
||||
var parseObject = new Parse.Object(testClassName);
|
||||
parseObject._finishFetch({
|
||||
key: 'value',
|
||||
className: testClassName
|
||||
});
|
||||
// Make mock message
|
||||
var message = {
|
||||
currentParseObject: parseObject
|
||||
};
|
||||
// Make sure we do not crash in this case
|
||||
parseLiveQueryServer._onAfterDelete(message, {});
|
||||
});
|
||||
|
||||
it('can handle object delete command which does not match any subscription', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Make deletedParseObject
|
||||
var parseObject = new Parse.Object(testClassName);
|
||||
parseObject._finishFetch({
|
||||
key: 'value',
|
||||
className: testClassName
|
||||
});
|
||||
// Make mock message
|
||||
var message = {
|
||||
currentParseObject: parseObject
|
||||
};
|
||||
|
||||
// Add mock client
|
||||
var clientId = 1;
|
||||
addMockClient(parseLiveQueryServer, clientId);
|
||||
// Add mock subscription
|
||||
var requestId = 2;
|
||||
addMockSubscription(parseLiveQueryServer, clientId, requestId);
|
||||
var client = parseLiveQueryServer.clients.get(clientId);
|
||||
// Mock _matchesSubscription to return not matching
|
||||
parseLiveQueryServer._matchesSubscription = function() {
|
||||
return false;
|
||||
};
|
||||
parseLiveQueryServer._matchesACL = function() {
|
||||
return true;
|
||||
};
|
||||
parseLiveQueryServer._onAfterDelete(message);
|
||||
|
||||
// Make sure we do not send command to client
|
||||
expect(client.pushDelete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can handle object delete command which matches some subscriptions', function(done) {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Make deletedParseObject
|
||||
var parseObject = new Parse.Object(testClassName);
|
||||
parseObject._finishFetch({
|
||||
key: 'value',
|
||||
className: testClassName
|
||||
});
|
||||
// Make mock message
|
||||
var message = {
|
||||
currentParseObject: parseObject
|
||||
};
|
||||
// Add mock client
|
||||
var clientId = 1;
|
||||
addMockClient(parseLiveQueryServer, clientId);
|
||||
// Add mock subscription
|
||||
var requestId = 2;
|
||||
addMockSubscription(parseLiveQueryServer, clientId, requestId);
|
||||
var client = parseLiveQueryServer.clients.get(clientId);
|
||||
// Mock _matchesSubscription to return matching
|
||||
parseLiveQueryServer._matchesSubscription = function() {
|
||||
return true;
|
||||
};
|
||||
parseLiveQueryServer._matchesACL = function() {
|
||||
return Parse.Promise.as(true);
|
||||
};
|
||||
parseLiveQueryServer._onAfterDelete(message);
|
||||
|
||||
// Make sure we send command to client, since _matchesACL is async, we have to
|
||||
// wait and check
|
||||
setTimeout(function() {
|
||||
expect(client.pushDelete).toHaveBeenCalled();
|
||||
done();
|
||||
}, jasmine.ASYNC_TEST_WAIT_TIME);
|
||||
});
|
||||
|
||||
it('has no subscription and can handle object save command', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Make mock request message
|
||||
var message = generateMockMessage();
|
||||
// Make sure we do not crash in this case
|
||||
parseLiveQueryServer._onAfterSave(message);
|
||||
});
|
||||
|
||||
it('can handle object save command which does not match any subscription', function(done) {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Make mock request message
|
||||
var message = generateMockMessage();
|
||||
// Add mock client
|
||||
var clientId = 1;
|
||||
var client = addMockClient(parseLiveQueryServer, clientId);
|
||||
// Add mock subscription
|
||||
var requestId = 2;
|
||||
addMockSubscription(parseLiveQueryServer, clientId, requestId);
|
||||
// Mock _matchesSubscription to return not matching
|
||||
parseLiveQueryServer._matchesSubscription = function() {
|
||||
return false;
|
||||
};
|
||||
parseLiveQueryServer._matchesACL = function() {
|
||||
return Parse.Promise.as(true)
|
||||
};
|
||||
// Trigger onAfterSave
|
||||
parseLiveQueryServer._onAfterSave(message);
|
||||
|
||||
// Make sure we do not send command to client
|
||||
setTimeout(function(){
|
||||
expect(client.pushCreate).not.toHaveBeenCalled();
|
||||
expect(client.pushEnter).not.toHaveBeenCalled();
|
||||
expect(client.pushUpdate).not.toHaveBeenCalled();
|
||||
expect(client.pushDelete).not.toHaveBeenCalled();
|
||||
expect(client.pushLeave).not.toHaveBeenCalled();
|
||||
done();
|
||||
}, jasmine.ASYNC_TEST_WAIT_TIME);
|
||||
});
|
||||
|
||||
it('can handle object enter command which matches some subscriptions', function(done) {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Make mock request message
|
||||
var message = generateMockMessage(true);
|
||||
// Add mock client
|
||||
var clientId = 1;
|
||||
var client = addMockClient(parseLiveQueryServer, clientId);
|
||||
// Add mock subscription
|
||||
var requestId = 2;
|
||||
addMockSubscription(parseLiveQueryServer, clientId, requestId);
|
||||
// Mock _matchesSubscription to return matching
|
||||
// In order to mimic a enter, we need original match return false
|
||||
// and the current match return true
|
||||
var counter = 0;
|
||||
parseLiveQueryServer._matchesSubscription = function(parseObject, subscription){
|
||||
if (!parseObject) {
|
||||
return false;
|
||||
}
|
||||
counter += 1;
|
||||
return counter % 2 === 0;
|
||||
};
|
||||
parseLiveQueryServer._matchesACL = function() {
|
||||
return Parse.Promise.as(true)
|
||||
};
|
||||
parseLiveQueryServer._onAfterSave(message);
|
||||
|
||||
// Make sure we send enter command to client
|
||||
setTimeout(function(){
|
||||
expect(client.pushCreate).not.toHaveBeenCalled();
|
||||
expect(client.pushEnter).toHaveBeenCalled();
|
||||
expect(client.pushUpdate).not.toHaveBeenCalled();
|
||||
expect(client.pushDelete).not.toHaveBeenCalled();
|
||||
expect(client.pushLeave).not.toHaveBeenCalled();
|
||||
done();
|
||||
}, jasmine.ASYNC_TEST_WAIT_TIME);
|
||||
});
|
||||
|
||||
it('can handle object update command which matches some subscriptions', function(done) {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Make mock request message
|
||||
var message = generateMockMessage(true);
|
||||
// Add mock client
|
||||
var clientId = 1;
|
||||
var client = addMockClient(parseLiveQueryServer, clientId);
|
||||
// Add mock subscription
|
||||
var requestId = 2;
|
||||
addMockSubscription(parseLiveQueryServer, clientId, requestId);
|
||||
// Mock _matchesSubscription to return matching
|
||||
parseLiveQueryServer._matchesSubscription = function(parseObject, subscription){
|
||||
if (!parseObject) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
parseLiveQueryServer._matchesACL = function() {
|
||||
return Parse.Promise.as(true)
|
||||
};
|
||||
parseLiveQueryServer._onAfterSave(message);
|
||||
|
||||
// Make sure we send update command to client
|
||||
setTimeout(function(){
|
||||
expect(client.pushCreate).not.toHaveBeenCalled();
|
||||
expect(client.pushEnter).not.toHaveBeenCalled();
|
||||
expect(client.pushUpdate).toHaveBeenCalled();
|
||||
expect(client.pushDelete).not.toHaveBeenCalled();
|
||||
expect(client.pushLeave).not.toHaveBeenCalled();
|
||||
done();
|
||||
}, jasmine.ASYNC_TEST_WAIT_TIME);
|
||||
});
|
||||
|
||||
it('can handle object leave command which matches some subscriptions', function(done) {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Make mock request message
|
||||
var message = generateMockMessage(true);
|
||||
// Add mock client
|
||||
var clientId = 1;
|
||||
var client = addMockClient(parseLiveQueryServer, clientId);
|
||||
// Add mock subscription
|
||||
var requestId = 2;
|
||||
addMockSubscription(parseLiveQueryServer, clientId, requestId);
|
||||
// Mock _matchesSubscription to return matching
|
||||
// In order to mimic a leave, we need original match return true
|
||||
// and the current match return false
|
||||
var counter = 0;
|
||||
parseLiveQueryServer._matchesSubscription = function(parseObject, subscription){
|
||||
if (!parseObject) {
|
||||
return false;
|
||||
}
|
||||
counter += 1;
|
||||
return counter % 2 !== 0;
|
||||
};
|
||||
parseLiveQueryServer._matchesACL = function() {
|
||||
return Parse.Promise.as(true)
|
||||
};
|
||||
parseLiveQueryServer._onAfterSave(message);
|
||||
|
||||
// Make sure we send leave command to client
|
||||
setTimeout(function(){
|
||||
expect(client.pushCreate).not.toHaveBeenCalled();
|
||||
expect(client.pushEnter).not.toHaveBeenCalled();
|
||||
expect(client.pushUpdate).not.toHaveBeenCalled();
|
||||
expect(client.pushDelete).not.toHaveBeenCalled();
|
||||
expect(client.pushLeave).toHaveBeenCalled();
|
||||
done();
|
||||
}, jasmine.ASYNC_TEST_WAIT_TIME);
|
||||
});
|
||||
|
||||
it('can handle object create command which matches some subscriptions', function(done) {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Make mock request message
|
||||
var message = generateMockMessage();
|
||||
// Add mock client
|
||||
var clientId = 1;
|
||||
var client = addMockClient(parseLiveQueryServer, clientId);
|
||||
// Add mock subscription
|
||||
var requestId = 2;
|
||||
addMockSubscription(parseLiveQueryServer, clientId, requestId);
|
||||
// Mock _matchesSubscription to return matching
|
||||
parseLiveQueryServer._matchesSubscription = function(parseObject, subscription){
|
||||
if (!parseObject) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
parseLiveQueryServer._matchesACL = function() {
|
||||
return Parse.Promise.as(true)
|
||||
};
|
||||
parseLiveQueryServer._onAfterSave(message);
|
||||
|
||||
// Make sure we send create command to client
|
||||
setTimeout(function(){
|
||||
expect(client.pushCreate).toHaveBeenCalled();
|
||||
expect(client.pushEnter).not.toHaveBeenCalled();
|
||||
expect(client.pushUpdate).not.toHaveBeenCalled();
|
||||
expect(client.pushDelete).not.toHaveBeenCalled();
|
||||
expect(client.pushLeave).not.toHaveBeenCalled();
|
||||
done();
|
||||
}, jasmine.ASYNC_TEST_WAIT_TIME);
|
||||
});
|
||||
|
||||
it('can match subscription for null or undefined parse object', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Make mock subscription
|
||||
var subscription = {
|
||||
match: jasmine.createSpy('match')
|
||||
}
|
||||
|
||||
expect(parseLiveQueryServer._matchesSubscription(null, subscription)).toBe(false);
|
||||
expect(parseLiveQueryServer._matchesSubscription(undefined, subscription)).toBe(false);
|
||||
// Make sure subscription.match is not called
|
||||
expect(subscription.match).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can match subscription', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Make mock subscription
|
||||
var subscription = {
|
||||
query: {}
|
||||
}
|
||||
var parseObject = {};
|
||||
expect(parseLiveQueryServer._matchesSubscription(parseObject, subscription)).toBe(true);
|
||||
// Make sure matchesQuery is called
|
||||
var matchesQuery = require('../src/LiveQuery/QueryTools').matchesQuery;
|
||||
expect(matchesQuery).toHaveBeenCalledWith(parseObject, subscription.query);
|
||||
});
|
||||
|
||||
it('can inflate parse object', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
// Make mock request
|
||||
var objectJSON = {
|
||||
"className":"testClassName",
|
||||
"createdAt":"2015-12-22T01:51:12.955Z",
|
||||
"key":"value",
|
||||
"objectId":"BfwxBCz6yW",
|
||||
"updatedAt":"2016-01-05T00:46:45.659Z"
|
||||
};
|
||||
var originalObjectJSON = {
|
||||
"className":"testClassName",
|
||||
"createdAt":"2015-12-22T01:51:12.955Z",
|
||||
"key":"originalValue",
|
||||
"objectId":"BfwxBCz6yW",
|
||||
"updatedAt":"2016-01-05T00:46:45.659Z"
|
||||
};
|
||||
var message = {
|
||||
currentParseObject: objectJSON,
|
||||
originalParseObject: originalObjectJSON
|
||||
};
|
||||
// Inflate the object
|
||||
parseLiveQueryServer._inflateParseObject(message);
|
||||
|
||||
// Verify object
|
||||
var object = message.currentParseObject;
|
||||
expect(object instanceof Parse.Object).toBeTruthy();
|
||||
expect(object.get('key')).toEqual('value');
|
||||
expect(object.className).toEqual('testClassName');
|
||||
expect(object.id).toBe('BfwxBCz6yW');
|
||||
expect(object.createdAt).not.toBeUndefined();
|
||||
expect(object.updatedAt).not.toBeUndefined();
|
||||
// Verify original object
|
||||
var originalObject = message.originalParseObject;
|
||||
expect(originalObject instanceof Parse.Object).toBeTruthy();
|
||||
expect(originalObject.get('key')).toEqual('originalValue');
|
||||
expect(originalObject.className).toEqual('testClassName');
|
||||
expect(originalObject.id).toBe('BfwxBCz6yW');
|
||||
expect(originalObject.createdAt).not.toBeUndefined();
|
||||
expect(originalObject.updatedAt).not.toBeUndefined();
|
||||
});
|
||||
|
||||
it('can match undefined ACL', function(done) {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
var client = {};
|
||||
var requestId = 0;
|
||||
|
||||
parseLiveQueryServer._matchesACL(undefined, client, requestId).then(function(isMatched) {
|
||||
expect(isMatched).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can match ACL with none exist requestId', function(done) {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
var acl = new Parse.ACL();
|
||||
var client = {
|
||||
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue(undefined)
|
||||
};
|
||||
var requestId = 0;
|
||||
|
||||
var isChecked = false;
|
||||
parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) {
|
||||
expect(isMatched).toBe(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can match ACL with public read access', function(done) {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
var acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
var client = {
|
||||
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
|
||||
sessionToken: 'sessionToken'
|
||||
})
|
||||
};
|
||||
var requestId = 0;
|
||||
|
||||
parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) {
|
||||
expect(isMatched).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can match ACL with valid subscription sessionToken', function(done) {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
var acl = new Parse.ACL();
|
||||
acl.setReadAccess(testUserId, true);
|
||||
var client = {
|
||||
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
|
||||
sessionToken: 'sessionToken'
|
||||
})
|
||||
};
|
||||
var requestId = 0;
|
||||
|
||||
parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) {
|
||||
expect(isMatched).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can match ACL with valid client sessionToken', function(done) {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
var acl = new Parse.ACL();
|
||||
acl.setReadAccess(testUserId, true);
|
||||
// Mock sessionTokenCache will return false when sessionToken is undefined
|
||||
var client = {
|
||||
sessionToken: 'sessionToken',
|
||||
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
|
||||
sessionToken: undefined
|
||||
})
|
||||
};
|
||||
var requestId = 0;
|
||||
|
||||
parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) {
|
||||
expect(isMatched).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can match ACL with invalid subscription and client sessionToken', function(done) {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
var acl = new Parse.ACL();
|
||||
acl.setReadAccess(testUserId, true);
|
||||
// Mock sessionTokenCache will return false when sessionToken is undefined
|
||||
var client = {
|
||||
sessionToken: undefined,
|
||||
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
|
||||
sessionToken: undefined
|
||||
})
|
||||
};
|
||||
var requestId = 0;
|
||||
|
||||
parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) {
|
||||
expect(isMatched).toBe(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can match ACL with subscription sessionToken checking error', function(done) {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
var acl = new Parse.ACL();
|
||||
acl.setReadAccess(testUserId, true);
|
||||
// Mock sessionTokenCache will return error when sessionToken is null, this is just
|
||||
// the behaviour of our mock sessionTokenCache, not real sessionTokenCache
|
||||
var client = {
|
||||
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
|
||||
sessionToken: null
|
||||
})
|
||||
};
|
||||
var requestId = 0;
|
||||
|
||||
parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) {
|
||||
expect(isMatched).toBe(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can match ACL with client sessionToken checking error', function(done) {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
var acl = new Parse.ACL();
|
||||
acl.setReadAccess(testUserId, true);
|
||||
// Mock sessionTokenCache will return error when sessionToken is null
|
||||
var client = {
|
||||
sessionToken: null,
|
||||
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
|
||||
sessionToken: null
|
||||
})
|
||||
};
|
||||
var requestId = 0;
|
||||
|
||||
parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) {
|
||||
expect(isMatched).toBe(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can validate key when valid key is provided', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer({}, {
|
||||
keyPairs: {
|
||||
clientKey: 'test'
|
||||
}
|
||||
});
|
||||
var request = {
|
||||
clientKey: 'test'
|
||||
}
|
||||
|
||||
expect(parseLiveQueryServer._validateKeys(request, parseLiveQueryServer.keyPairs)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('can validate key when invalid key is provided', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer({}, {
|
||||
keyPairs: {
|
||||
clientKey: 'test'
|
||||
}
|
||||
});
|
||||
var request = {
|
||||
clientKey: 'error'
|
||||
}
|
||||
|
||||
expect(parseLiveQueryServer._validateKeys(request, parseLiveQueryServer.keyPairs)).not.toBeTruthy();
|
||||
});
|
||||
|
||||
it('can validate key when key is not provided', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer({}, {
|
||||
keyPairs: {
|
||||
clientKey: 'test'
|
||||
}
|
||||
});
|
||||
var request = {
|
||||
}
|
||||
|
||||
expect(parseLiveQueryServer._validateKeys(request, parseLiveQueryServer.keyPairs)).not.toBeTruthy();
|
||||
});
|
||||
|
||||
it('can validate key when validKerPairs is empty', function() {
|
||||
var parseLiveQueryServer = new ParseLiveQueryServer({}, {});
|
||||
var request = {
|
||||
}
|
||||
|
||||
expect(parseLiveQueryServer._validateKeys(request, parseLiveQueryServer.keyPairs)).toBeTruthy();
|
||||
});
|
||||
|
||||
afterEach(function(){
|
||||
jasmine.restoreLibrary('../src/LiveQuery/ParseWebSocketServer', 'ParseWebSocketServer');
|
||||
jasmine.restoreLibrary('../src/LiveQuery/Client', 'Client');
|
||||
jasmine.restoreLibrary('../src/LiveQuery/Subscription', 'Subscription');
|
||||
jasmine.restoreLibrary('../src/LiveQuery/QueryTools', 'queryHash');
|
||||
jasmine.restoreLibrary('../src/LiveQuery/QueryTools', 'matchesQuery');
|
||||
jasmine.restoreLibrary('tv4', 'validate');
|
||||
jasmine.restoreLibrary('../src/LiveQuery/ParsePubSub', 'ParsePubSub');
|
||||
jasmine.restoreLibrary('../src/LiveQuery/SessionTokenCache', 'SessionTokenCache');
|
||||
});
|
||||
|
||||
// Helper functions to add mock client and subscription to a liveQueryServer
|
||||
function addMockClient(parseLiveQueryServer, clientId) {
|
||||
var Client = require('../src/LiveQuery/Client').Client;
|
||||
var client = new Client(clientId, {});
|
||||
parseLiveQueryServer.clients.set(clientId, client);
|
||||
return client;
|
||||
}
|
||||
|
||||
function addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket, query) {
|
||||
// If parseWebSocket is null, we use the default one
|
||||
if (!parseWebSocket) {
|
||||
var EventEmitter = require('events');
|
||||
parseWebSocket = new EventEmitter();
|
||||
}
|
||||
parseWebSocket.clientId = clientId;
|
||||
// If query is null, we use the default one
|
||||
if (!query) {
|
||||
query = {
|
||||
className: testClassName,
|
||||
where: {
|
||||
key: 'value'
|
||||
},
|
||||
fields: [ 'test' ]
|
||||
};
|
||||
}
|
||||
var request = {
|
||||
query: query,
|
||||
requestId: requestId,
|
||||
sessionToken: 'sessionToken'
|
||||
};
|
||||
parseLiveQueryServer._handleSubscribe(parseWebSocket, request);
|
||||
|
||||
// Make mock subscription
|
||||
var subscription = parseLiveQueryServer.subscriptions.get(query.className).get(queryHashValue);
|
||||
subscription.hasSubscribingClient = function() {
|
||||
return false;
|
||||
}
|
||||
subscription.className = query.className;
|
||||
subscription.hash = queryHashValue;
|
||||
if (subscription.clientRequestIds && subscription.clientRequestIds.has(clientId)) {
|
||||
subscription.clientRequestIds.get(clientId).push(requestId);
|
||||
} else {
|
||||
subscription.clientRequestIds = new Map([[clientId, [requestId]]]);
|
||||
}
|
||||
return subscription;
|
||||
}
|
||||
|
||||
// Helper functiosn to generate request message
|
||||
function generateMockMessage(hasOriginalParseObject) {
|
||||
var parseObject = new Parse.Object(testClassName);
|
||||
parseObject._finishFetch({
|
||||
key: 'value',
|
||||
className: testClassName
|
||||
});
|
||||
var message = {
|
||||
currentParseObject: parseObject
|
||||
};
|
||||
if (hasOriginalParseObject) {
|
||||
var originalParseObject = new Parse.Object(testClassName);
|
||||
originalParseObject._finishFetch({
|
||||
key: 'originalValue',
|
||||
className: testClassName
|
||||
});
|
||||
message.originalParseObject = originalParseObject;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
});
|
||||
65
spec/ParsePubSub.spec.js
Normal file
65
spec/ParsePubSub.spec.js
Normal file
@@ -0,0 +1,65 @@
|
||||
var ParsePubSub = require('../src/LiveQuery/ParsePubSub').ParsePubSub;
|
||||
|
||||
describe('ParsePubSub', function() {
|
||||
|
||||
beforeEach(function(done) {
|
||||
// Mock RedisPubSub
|
||||
var mockRedisPubSub = {
|
||||
createPublisher: jasmine.createSpy('createPublisherRedis'),
|
||||
createSubscriber: jasmine.createSpy('createSubscriberRedis')
|
||||
};
|
||||
jasmine.mockLibrary('../src/LiveQuery/RedisPubSub', 'RedisPubSub', mockRedisPubSub);
|
||||
// Mock EventEmitterPubSub
|
||||
var mockEventEmitterPubSub = {
|
||||
createPublisher: jasmine.createSpy('createPublisherEventEmitter'),
|
||||
createSubscriber: jasmine.createSpy('createSubscriberEventEmitter')
|
||||
};
|
||||
jasmine.mockLibrary('../src/LiveQuery/EventEmitterPubSub', 'EventEmitterPubSub', mockEventEmitterPubSub);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can create redis publisher', function() {
|
||||
var publisher = ParsePubSub.createPublisher({
|
||||
redisURL: 'redisURL'
|
||||
});
|
||||
|
||||
var RedisPubSub = require('../src/LiveQuery/RedisPubSub').RedisPubSub;
|
||||
var EventEmitterPubSub = require('../src/LiveQuery/EventEmitterPubSub').EventEmitterPubSub;
|
||||
expect(RedisPubSub.createPublisher).toHaveBeenCalledWith('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;
|
||||
expect(RedisPubSub.createPublisher).not.toHaveBeenCalled();
|
||||
expect(EventEmitterPubSub.createPublisher).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can create redis subscriber', function() {
|
||||
var subscriber = ParsePubSub.createSubscriber({
|
||||
redisURL: 'redisURL'
|
||||
});
|
||||
|
||||
var RedisPubSub = require('../src/LiveQuery/RedisPubSub').RedisPubSub;
|
||||
var EventEmitterPubSub = require('../src/LiveQuery/EventEmitterPubSub').EventEmitterPubSub;
|
||||
expect(RedisPubSub.createSubscriber).toHaveBeenCalledWith('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;
|
||||
expect(RedisPubSub.createSubscriber).not.toHaveBeenCalled();
|
||||
expect(EventEmitterPubSub.createSubscriber).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
afterEach(function(){
|
||||
jasmine.restoreLibrary('../src/LiveQuery/RedisPubSub', 'RedisPubSub');
|
||||
jasmine.restoreLibrary('../src/LiveQuery/EventEmitterPubSub', 'EventEmitterPubSub');
|
||||
});
|
||||
});
|
||||
43
spec/ParseWebSocket.spec.js
Normal file
43
spec/ParseWebSocket.spec.js
Normal file
@@ -0,0 +1,43 @@
|
||||
var ParseWebSocket = require('../src/LiveQuery/ParseWebSocketServer').ParseWebSocket;
|
||||
|
||||
describe('ParseWebSocket', function() {
|
||||
|
||||
it('can be initialized', function() {
|
||||
var ws = {};
|
||||
var parseWebSocket = new ParseWebSocket(ws);
|
||||
|
||||
expect(parseWebSocket.ws).toBe(ws);
|
||||
});
|
||||
|
||||
it('can handle events defined in typeMap', function() {
|
||||
var ws = {
|
||||
on: jasmine.createSpy('on')
|
||||
};
|
||||
var callback = {};
|
||||
var parseWebSocket = new ParseWebSocket(ws);
|
||||
parseWebSocket.on('disconnect', callback);
|
||||
|
||||
expect(parseWebSocket.ws.on).toHaveBeenCalledWith('close', callback);
|
||||
});
|
||||
|
||||
it('can handle events which are not defined in typeMap', function() {
|
||||
var ws = {
|
||||
on: jasmine.createSpy('on')
|
||||
};
|
||||
var callback = {};
|
||||
var parseWebSocket = new ParseWebSocket(ws);
|
||||
parseWebSocket.on('open', callback);
|
||||
|
||||
expect(parseWebSocket.ws.on).toHaveBeenCalledWith('open', callback);
|
||||
});
|
||||
|
||||
it('can send a message', function() {
|
||||
var ws = {
|
||||
send: jasmine.createSpy('send')
|
||||
};
|
||||
var parseWebSocket = new ParseWebSocket(ws);
|
||||
parseWebSocket.send('message')
|
||||
|
||||
expect(parseWebSocket.ws.send).toHaveBeenCalledWith('message');
|
||||
});
|
||||
});
|
||||
37
spec/ParseWebSocketServer.spec.js
Normal file
37
spec/ParseWebSocketServer.spec.js
Normal file
@@ -0,0 +1,37 @@
|
||||
var ParseWebSocketServer = require('../src/LiveQuery/ParseWebSocketServer').ParseWebSocketServer;
|
||||
|
||||
describe('ParseWebSocketServer', function() {
|
||||
|
||||
beforeEach(function(done) {
|
||||
// Mock ws server
|
||||
var EventEmitter = require('events');
|
||||
var mockServer = function() {
|
||||
return new EventEmitter();
|
||||
};
|
||||
jasmine.mockLibrary('ws', 'Server', mockServer);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can handle connect event when ws is open', function(done) {
|
||||
var onConnectCallback = jasmine.createSpy('onConnectCallback');
|
||||
var parseWebSocketServer = new ParseWebSocketServer({}, onConnectCallback, 5).server;
|
||||
var ws = {
|
||||
readyState: 0,
|
||||
OPEN: 0,
|
||||
ping: jasmine.createSpy('ping')
|
||||
};
|
||||
parseWebSocketServer.emit('connection', ws);
|
||||
|
||||
// Make sure callback is called
|
||||
expect(onConnectCallback).toHaveBeenCalled();
|
||||
// Make sure we ping to the client
|
||||
setTimeout(function() {
|
||||
expect(ws.ping).toHaveBeenCalled();
|
||||
done();
|
||||
}, 10)
|
||||
});
|
||||
|
||||
afterEach(function(){
|
||||
jasmine.restoreLibrary('ws', 'Server');
|
||||
});
|
||||
});
|
||||
384
spec/QueryTools.spec.js
Normal file
384
spec/QueryTools.spec.js
Normal file
@@ -0,0 +1,384 @@
|
||||
var Parse = require('parse/node');
|
||||
|
||||
var Id = require('../src/LiveQuery/Id');
|
||||
var QueryTools = require('../src/LiveQuery/QueryTools');
|
||||
var queryHash = QueryTools.queryHash;
|
||||
var matchesQuery = QueryTools.matchesQuery;
|
||||
|
||||
var Item = Parse.Object.extend('Item');
|
||||
|
||||
describe('queryHash', function() {
|
||||
|
||||
it('should always hash a query to the same string', function() {
|
||||
var q = new Parse.Query(Item);
|
||||
q.equalTo('field', 'value');
|
||||
q.exists('name');
|
||||
q.ascending('createdAt');
|
||||
q.limit(10);
|
||||
var firstHash = queryHash(q);
|
||||
var secondHash = queryHash(q);
|
||||
expect(firstHash).toBe(secondHash);
|
||||
});
|
||||
|
||||
it('should return equivalent hashes for equivalent queries', function() {
|
||||
var q1 = new Parse.Query(Item);
|
||||
q1.equalTo('field', 'value');
|
||||
q1.exists('name');
|
||||
q1.lessThan('age', 30);
|
||||
q1.greaterThan('age', 3);
|
||||
q1.ascending('createdAt');
|
||||
q1.include(['name', 'age']);
|
||||
q1.limit(10);
|
||||
|
||||
var q2 = new Parse.Query(Item);
|
||||
q2.limit(10);
|
||||
q2.greaterThan('age', 3);
|
||||
q2.lessThan('age', 30);
|
||||
q2.include(['name', 'age']);
|
||||
q2.ascending('createdAt');
|
||||
q2.exists('name');
|
||||
q2.equalTo('field', 'value');
|
||||
|
||||
var firstHash = queryHash(q1);
|
||||
var secondHash = queryHash(q2);
|
||||
expect(firstHash).toBe(secondHash);
|
||||
|
||||
q1.containedIn('fruit', ['apple', 'banana', 'cherry']);
|
||||
firstHash = queryHash(q1);
|
||||
expect(firstHash).not.toBe(secondHash);
|
||||
|
||||
q2.containedIn('fruit', ['banana', 'cherry', 'apple']);
|
||||
secondHash = queryHash(q2);
|
||||
expect(secondHash).toBe(firstHash);
|
||||
|
||||
q1.containedIn('fruit', ['coconut']);
|
||||
firstHash = queryHash(q1);
|
||||
expect(firstHash).not.toBe(secondHash);
|
||||
|
||||
q1 = new Parse.Query(Item);
|
||||
q1.equalTo('field', 'value');
|
||||
q1.lessThan('age', 30);
|
||||
q1.exists('name');
|
||||
|
||||
q2 = new Parse.Query(Item);
|
||||
q2.equalTo('name', 'person');
|
||||
q2.equalTo('field', 'other');
|
||||
|
||||
firstHash = queryHash(Parse.Query.or(q1, q2));
|
||||
secondHash = queryHash(Parse.Query.or(q2, q1));
|
||||
expect(firstHash).toBe(secondHash);
|
||||
});
|
||||
|
||||
it('should not let fields of different types appear similar', function() {
|
||||
var q1 = new Parse.Query(Item);
|
||||
q1.lessThan('age', 30);
|
||||
|
||||
var q2 = new Parse.Query(Item);
|
||||
q2.equalTo('age', '{$lt:30}');
|
||||
|
||||
expect(queryHash(q1)).not.toBe(queryHash(q2));
|
||||
|
||||
q1 = new Parse.Query(Item);
|
||||
q1.equalTo('age', 15);
|
||||
|
||||
q2.equalTo('age', '15');
|
||||
|
||||
expect(queryHash(q1)).not.toBe(queryHash(q2));
|
||||
});
|
||||
});
|
||||
|
||||
describe('matchesQuery', function() {
|
||||
it('matches blanket queries', function() {
|
||||
var obj = {
|
||||
id: new Id('Klass', 'O1'),
|
||||
value: 12
|
||||
};
|
||||
var q = new Parse.Query('Klass');
|
||||
expect(matchesQuery(obj, q)).toBe(true);
|
||||
|
||||
obj.id = new Id('Other', 'O1');
|
||||
expect(matchesQuery(obj, q)).toBe(false);
|
||||
});
|
||||
|
||||
it('matches existence queries', function() {
|
||||
var obj = {
|
||||
id: new Id('Item', 'O1'),
|
||||
count: 15
|
||||
};
|
||||
var q = new Parse.Query('Item');
|
||||
q.exists('count');
|
||||
expect(matchesQuery(obj, q)).toBe(true);
|
||||
q.exists('name');
|
||||
expect(matchesQuery(obj, q)).toBe(false);
|
||||
});
|
||||
|
||||
it('matches on equality queries', function() {
|
||||
var day = new Date();
|
||||
var location = new Parse.GeoPoint({
|
||||
latitude: 37.484815,
|
||||
longitude: -122.148377
|
||||
});
|
||||
var obj = {
|
||||
id: new Id('Person', 'O1'),
|
||||
score: 12,
|
||||
name: 'Bill',
|
||||
birthday: day,
|
||||
lastLocation: location
|
||||
};
|
||||
|
||||
var q = new Parse.Query('Person');
|
||||
q.equalTo('score', 12);
|
||||
expect(matchesQuery(obj, q)).toBe(true);
|
||||
|
||||
q = new Parse.Query('Person');
|
||||
q.equalTo('name', 'Bill');
|
||||
expect(matchesQuery(obj, q)).toBe(true);
|
||||
q.equalTo('name', 'Jeff');
|
||||
expect(matchesQuery(obj, q)).toBe(false);
|
||||
|
||||
q = new Parse.Query('Person');
|
||||
q.containedIn('name', ['Adam', 'Ben', 'Charles']);
|
||||
expect(matchesQuery(obj, q)).toBe(false);
|
||||
q.containedIn('name', ['Adam', 'Bill', 'Charles']);
|
||||
expect(matchesQuery(obj, q)).toBe(true);
|
||||
|
||||
q = new Parse.Query('Person');
|
||||
q.notContainedIn('name', ['Adam', 'Bill', 'Charles']);
|
||||
expect(matchesQuery(obj, q)).toBe(false);
|
||||
q.notContainedIn('name', ['Adam', 'Ben', 'Charles']);
|
||||
expect(matchesQuery(obj, q)).toBe(true);
|
||||
|
||||
q = new Parse.Query('Person');
|
||||
q.equalTo('birthday', day);
|
||||
expect(matchesQuery(obj, q)).toBe(true);
|
||||
q.equalTo('birthday', new Date());
|
||||
expect(matchesQuery(obj, q)).toBe(false);
|
||||
|
||||
q = new Parse.Query('Person');
|
||||
q.equalTo('lastLocation', new Parse.GeoPoint({
|
||||
latitude: 37.484815,
|
||||
longitude: -122.148377
|
||||
}));
|
||||
expect(matchesQuery(obj, q)).toBe(true);
|
||||
q.equalTo('lastLocation', new Parse.GeoPoint({
|
||||
latitude: 37.4848,
|
||||
longitude: -122.1483
|
||||
}));
|
||||
expect(matchesQuery(obj, q)).toBe(false);
|
||||
|
||||
q.equalTo('lastLocation', new Parse.GeoPoint({
|
||||
latitude: 37.484815,
|
||||
longitude: -122.148377
|
||||
}));
|
||||
q.equalTo('score', 12);
|
||||
q.equalTo('name', 'Bill');
|
||||
q.equalTo('birthday', day);
|
||||
expect(matchesQuery(obj, q)).toBe(true);
|
||||
|
||||
q.equalTo('name', 'bill');
|
||||
expect(matchesQuery(obj, q)).toBe(false);
|
||||
|
||||
var img = {
|
||||
id: new Id('Image', 'I1'),
|
||||
tags: ['nofilter', 'latergram', 'tbt']
|
||||
};
|
||||
|
||||
q = new Parse.Query('Image');
|
||||
q.equalTo('tags', 'selfie');
|
||||
expect(matchesQuery(img, q)).toBe(false);
|
||||
q.equalTo('tags', 'tbt');
|
||||
expect(matchesQuery(img, q)).toBe(true);
|
||||
|
||||
var q2 = new Parse.Query('Image');
|
||||
q2.containsAll('tags', ['latergram', 'nofilter']);
|
||||
expect(matchesQuery(img, q2)).toBe(true);
|
||||
q2.containsAll('tags', ['latergram', 'selfie']);
|
||||
expect(matchesQuery(img, q2)).toBe(false);
|
||||
|
||||
var u = new Parse.User();
|
||||
u.id = 'U2';
|
||||
q = new Parse.Query('Image');
|
||||
q.equalTo('owner', u);
|
||||
|
||||
img = {
|
||||
className: 'Image',
|
||||
objectId: 'I1',
|
||||
owner: {
|
||||
className: '_User',
|
||||
objectId: 'U2'
|
||||
}
|
||||
};
|
||||
expect(matchesQuery(img, q)).toBe(true);
|
||||
|
||||
img.owner.objectId = 'U3';
|
||||
expect(matchesQuery(img, q)).toBe(false);
|
||||
});
|
||||
|
||||
it('matches on inequalities', function() {
|
||||
var player = {
|
||||
id: new Id('Person', 'O1'),
|
||||
score: 12,
|
||||
name: 'Bill',
|
||||
birthday: new Date(1980, 2, 4),
|
||||
};
|
||||
var q = new Parse.Query('Person');
|
||||
q.lessThan('score', 15);
|
||||
expect(matchesQuery(player, q)).toBe(true);
|
||||
q.lessThan('score', 10);
|
||||
expect(matchesQuery(player, q)).toBe(false);
|
||||
|
||||
q = new Parse.Query('Person');
|
||||
q.lessThanOrEqualTo('score', 15);
|
||||
expect(matchesQuery(player, q)).toBe(true);
|
||||
q.lessThanOrEqualTo('score', 12);
|
||||
expect(matchesQuery(player, q)).toBe(true);
|
||||
q.lessThanOrEqualTo('score', 10);
|
||||
expect(matchesQuery(player, q)).toBe(false);
|
||||
|
||||
q = new Parse.Query('Person');
|
||||
q.greaterThan('score', 15);
|
||||
expect(matchesQuery(player, q)).toBe(false);
|
||||
q.greaterThan('score', 10);
|
||||
expect(matchesQuery(player, q)).toBe(true);
|
||||
|
||||
q = new Parse.Query('Person');
|
||||
q.greaterThanOrEqualTo('score', 15);
|
||||
expect(matchesQuery(player, q)).toBe(false);
|
||||
q.greaterThanOrEqualTo('score', 12);
|
||||
expect(matchesQuery(player, q)).toBe(true);
|
||||
q.greaterThanOrEqualTo('score', 10);
|
||||
expect(matchesQuery(player, q)).toBe(true);
|
||||
|
||||
q = new Parse.Query('Person');
|
||||
q.notEqualTo('score', 12);
|
||||
expect(matchesQuery(player, q)).toBe(false);
|
||||
q.notEqualTo('score', 40);
|
||||
expect(matchesQuery(player, q)).toBe(true);
|
||||
});
|
||||
|
||||
it('matches an $or query', function() {
|
||||
var player = {
|
||||
id: new Id('Player', 'P1'),
|
||||
name: 'Player 1',
|
||||
score: 12
|
||||
};
|
||||
var q = new Parse.Query('Player');
|
||||
q.equalTo('name', 'Player 1');
|
||||
var q2 = new Parse.Query('Player');
|
||||
q2.equalTo('name', 'Player 2');
|
||||
var orQuery = Parse.Query.or(q, q2);
|
||||
expect(matchesQuery(player, q)).toBe(true);
|
||||
expect(matchesQuery(player, q2)).toBe(false);
|
||||
expect(matchesQuery(player, orQuery)).toBe(true);
|
||||
});
|
||||
|
||||
it('matches $regex queries', function() {
|
||||
var player = {
|
||||
id: new Id('Player', 'P1'),
|
||||
name: 'Player 1',
|
||||
score: 12
|
||||
};
|
||||
|
||||
var q = new Parse.Query('Player');
|
||||
q.startsWith('name', 'Play');
|
||||
expect(matchesQuery(player, q)).toBe(true);
|
||||
q.startsWith('name', 'Ploy');
|
||||
expect(matchesQuery(player, q)).toBe(false);
|
||||
|
||||
q = new Parse.Query('Player');
|
||||
q.endsWith('name', ' 1');
|
||||
expect(matchesQuery(player, q)).toBe(true);
|
||||
q.endsWith('name', ' 2');
|
||||
expect(matchesQuery(player, q)).toBe(false);
|
||||
|
||||
// Check that special characters are escaped
|
||||
player.name = 'Android-7';
|
||||
q = new Parse.Query('Player');
|
||||
q.contains('name', 'd-7');
|
||||
expect(matchesQuery(player, q)).toBe(true);
|
||||
|
||||
q = new Parse.Query('Player');
|
||||
q.matches('name', /A.d/);
|
||||
expect(matchesQuery(player, q)).toBe(true);
|
||||
|
||||
q.matches('name', /A[^n]d/);
|
||||
expect(matchesQuery(player, q)).toBe(false);
|
||||
|
||||
// Check that the string \\E is returned to normal
|
||||
player.name = 'Slash \\E';
|
||||
q = new Parse.Query('Player');
|
||||
q.endsWith('name', 'h \\E');
|
||||
expect(matchesQuery(player, q)).toBe(true);
|
||||
|
||||
q.endsWith('name', 'h \\Ee');
|
||||
expect(matchesQuery(player, q)).toBe(false);
|
||||
|
||||
player.name = 'Slash \\Q and more';
|
||||
q = new Parse.Query('Player');
|
||||
q.contains('name', 'h \\Q and');
|
||||
expect(matchesQuery(player, q)).toBe(true);
|
||||
q.contains('name', 'h \\Q or');
|
||||
expect(matchesQuery(player, q)).toBe(false);
|
||||
});
|
||||
|
||||
it('matches $nearSphere queries', function() {
|
||||
var q = new Parse.Query('Checkin');
|
||||
q.near('location', new Parse.GeoPoint(20, 20));
|
||||
// With no max distance, any GeoPoint is 'near'
|
||||
var pt = {
|
||||
id: new Id('Checkin', 'C1'),
|
||||
location: new Parse.GeoPoint(40, 40)
|
||||
};
|
||||
expect(matchesQuery(pt, q)).toBe(true);
|
||||
|
||||
q = new Parse.Query('Checkin');
|
||||
pt.location = new Parse.GeoPoint(40, 40);
|
||||
q.withinRadians('location', new Parse.GeoPoint(30, 30), 0.3);
|
||||
expect(matchesQuery(pt, q)).toBe(true);
|
||||
|
||||
q.withinRadians('location', new Parse.GeoPoint(30, 30), 0.2);
|
||||
expect(matchesQuery(pt, q)).toBe(false);
|
||||
});
|
||||
|
||||
it('matches $within queries', function() {
|
||||
var caltrainStation = {
|
||||
id: new Id('Checkin', 'C1'),
|
||||
location: new Parse.GeoPoint(37.776346, -122.394218),
|
||||
name: 'Caltrain'
|
||||
};
|
||||
|
||||
var santaClara = {
|
||||
id: new Id('Checkin', 'C2'),
|
||||
location: new Parse.GeoPoint(37.325635, -121.945753),
|
||||
name: 'Santa Clara'
|
||||
};
|
||||
|
||||
var q = new Parse.Query('Checkin').withinGeoBox(
|
||||
'location',
|
||||
new Parse.GeoPoint(37.708813, -122.526398),
|
||||
new Parse.GeoPoint(37.822802, -122.373962)
|
||||
);
|
||||
|
||||
expect(matchesQuery(caltrainStation, q)).toBe(true);
|
||||
expect(matchesQuery(santaClara, q)).toBe(false);
|
||||
|
||||
// Invalid rectangles
|
||||
q = new Parse.Query('Checkin').withinGeoBox(
|
||||
'location',
|
||||
new Parse.GeoPoint(37.822802, -122.373962),
|
||||
new Parse.GeoPoint(37.708813, -122.526398)
|
||||
);
|
||||
|
||||
expect(matchesQuery(caltrainStation, q)).toBe(false);
|
||||
expect(matchesQuery(santaClara, q)).toBe(false);
|
||||
|
||||
q = new Parse.Query('Checkin').withinGeoBox(
|
||||
'location',
|
||||
new Parse.GeoPoint(37.708813, -122.373962),
|
||||
new Parse.GeoPoint(37.822802, -122.526398)
|
||||
);
|
||||
|
||||
expect(matchesQuery(caltrainStation, q)).toBe(false);
|
||||
expect(matchesQuery(santaClara, q)).toBe(false);
|
||||
});
|
||||
});
|
||||
29
spec/RedisPubSub.spec.js
Normal file
29
spec/RedisPubSub.spec.js
Normal file
@@ -0,0 +1,29 @@
|
||||
var RedisPubSub = require('../src/LiveQuery/RedisPubSub').RedisPubSub;
|
||||
|
||||
describe('RedisPubSub', function() {
|
||||
|
||||
beforeEach(function(done) {
|
||||
// Mock redis
|
||||
var createClient = jasmine.createSpy('createClient');
|
||||
jasmine.mockLibrary('redis', 'createClient', createClient);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can create publisher', function() {
|
||||
var publisher = RedisPubSub.createPublisher('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 redis = require('redis');
|
||||
expect(redis.createClient).toHaveBeenCalledWith('redisAddress', { no_ready_check: true });
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
jasmine.restoreLibrary('redis', 'createClient');
|
||||
});
|
||||
});
|
||||
52
spec/SessionTokenCache.spec.js
Normal file
52
spec/SessionTokenCache.spec.js
Normal file
@@ -0,0 +1,52 @@
|
||||
var SessionTokenCache = require('../src/LiveQuery/SessionTokenCache').SessionTokenCache;
|
||||
|
||||
describe('SessionTokenCache', function() {
|
||||
|
||||
beforeEach(function(done) {
|
||||
var Parse = require('parse/node');
|
||||
// Mock parse
|
||||
var mockUser = {
|
||||
become: jasmine.createSpy('become').and.returnValue(Parse.Promise.as({
|
||||
id: 'userId'
|
||||
}))
|
||||
}
|
||||
jasmine.mockLibrary('parse/node', 'User', mockUser);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can get undefined userId', function(done) {
|
||||
var sessionTokenCache = new SessionTokenCache();
|
||||
|
||||
sessionTokenCache.getUserId(undefined).then((userIdFromCache) => {
|
||||
}, (error) => {
|
||||
expect(error).not.toBeNull();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can get existing userId', function(done) {
|
||||
var sessionTokenCache = new SessionTokenCache();
|
||||
var sessionToken = 'sessionToken';
|
||||
var userId = 'userId'
|
||||
sessionTokenCache.cache.set(sessionToken, userId);
|
||||
|
||||
sessionTokenCache.getUserId(sessionToken).then((userIdFromCache) => {
|
||||
expect(userIdFromCache).toBe(userId);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can get new userId', function(done) {
|
||||
var sessionTokenCache = new SessionTokenCache();
|
||||
|
||||
sessionTokenCache.getUserId('sessionToken').then((userIdFromCache) => {
|
||||
expect(userIdFromCache).toBe('userId');
|
||||
expect(sessionTokenCache.cache.length).toBe(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
jasmine.restoreLibrary('parse/node', 'User');
|
||||
});
|
||||
});
|
||||
123
spec/Subscription.spec.js
Normal file
123
spec/Subscription.spec.js
Normal file
@@ -0,0 +1,123 @@
|
||||
var Subscription = require('../src/LiveQuery/Subscription').Subscription;
|
||||
|
||||
describe('Subscription', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
var mockError = jasmine.createSpy('error');
|
||||
jasmine.mockLibrary('../src/LiveQuery/PLog', 'error', mockError);
|
||||
});
|
||||
|
||||
it('can be initialized', function() {
|
||||
var subscription = new Subscription('className', { key : 'value' }, 'hash');
|
||||
|
||||
expect(subscription.className).toBe('className');
|
||||
expect(subscription.query).toEqual({ key : 'value' });
|
||||
expect(subscription.hash).toBe('hash');
|
||||
expect(subscription.clientRequestIds.size).toBe(0);
|
||||
});
|
||||
|
||||
it('can check it has subscribing clients', function() {
|
||||
var subscription = new Subscription('className', { key : 'value' }, 'hash');
|
||||
|
||||
expect(subscription.hasSubscribingClient()).toBe(false);
|
||||
});
|
||||
|
||||
it('can check it does not have subscribing clients', function() {
|
||||
var subscription = new Subscription('className', { key : 'value' }, 'hash');
|
||||
subscription.addClientSubscription(1, 1);
|
||||
|
||||
expect(subscription.hasSubscribingClient()).toBe(true);
|
||||
});
|
||||
|
||||
it('can add one request for one client', function() {
|
||||
var subscription = new Subscription('className', { key : 'value' }, 'hash');
|
||||
subscription.addClientSubscription(1, 1);
|
||||
|
||||
expect(subscription.clientRequestIds.size).toBe(1);
|
||||
expect(subscription.clientRequestIds.get(1)).toEqual([1]);
|
||||
});
|
||||
|
||||
it('can add requests for one client', function() {
|
||||
var subscription = new Subscription('className', { key : 'value' }, 'hash');
|
||||
subscription.addClientSubscription(1, 1);
|
||||
subscription.addClientSubscription(1, 2);
|
||||
|
||||
expect(subscription.clientRequestIds.size).toBe(1);
|
||||
expect(subscription.clientRequestIds.get(1)).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
it('can add requests for clients', function() {
|
||||
var subscription = new Subscription('className', { key : 'value' }, 'hash');
|
||||
subscription.addClientSubscription(1, 1);
|
||||
subscription.addClientSubscription(1, 2);
|
||||
subscription.addClientSubscription(2, 2);
|
||||
subscription.addClientSubscription(2, 3);
|
||||
|
||||
expect(subscription.clientRequestIds.size).toBe(2);
|
||||
expect(subscription.clientRequestIds.get(1)).toEqual([1, 2]);
|
||||
expect(subscription.clientRequestIds.get(2)).toEqual([2, 3]);
|
||||
});
|
||||
|
||||
it('can delete requests for nonexistent client', function() {
|
||||
var subscription = new Subscription('className', { key : 'value' }, 'hash');
|
||||
subscription.deleteClientSubscription(1, 1);
|
||||
|
||||
var PLog =require('../src/LiveQuery/PLog');
|
||||
expect(PLog.error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can delete nonexistent request for one client', function() {
|
||||
var subscription = new Subscription('className', { key : 'value' }, 'hash');
|
||||
subscription.addClientSubscription(1, 1);
|
||||
subscription.deleteClientSubscription(1, 2);
|
||||
|
||||
var PLog =require('../src/LiveQuery/PLog');
|
||||
expect(PLog.error).toHaveBeenCalled();
|
||||
expect(subscription.clientRequestIds.size).toBe(1);
|
||||
expect(subscription.clientRequestIds.get(1)).toEqual([1]);
|
||||
});
|
||||
|
||||
it('can delete some requests for one client', function() {
|
||||
var subscription = new Subscription('className', { key : 'value' }, 'hash');
|
||||
subscription.addClientSubscription(1, 1);
|
||||
subscription.addClientSubscription(1, 2);
|
||||
subscription.deleteClientSubscription(1, 2);
|
||||
|
||||
var PLog =require('../src/LiveQuery/PLog');
|
||||
expect(PLog.error).not.toHaveBeenCalled();
|
||||
expect(subscription.clientRequestIds.size).toBe(1);
|
||||
expect(subscription.clientRequestIds.get(1)).toEqual([1]);
|
||||
});
|
||||
|
||||
it('can delete all requests for one client', function() {
|
||||
var subscription = new Subscription('className', { key : 'value' }, 'hash');
|
||||
subscription.addClientSubscription(1, 1);
|
||||
subscription.addClientSubscription(1, 2);
|
||||
subscription.deleteClientSubscription(1, 1);
|
||||
subscription.deleteClientSubscription(1, 2);
|
||||
|
||||
var PLog =require('../src/LiveQuery/PLog');
|
||||
expect(PLog.error).not.toHaveBeenCalled();
|
||||
expect(subscription.clientRequestIds.size).toBe(0);
|
||||
});
|
||||
|
||||
it('can delete requests for multiple clients', function() {
|
||||
var subscription = new Subscription('className', { key : 'value' }, 'hash');
|
||||
subscription.addClientSubscription(1, 1);
|
||||
subscription.addClientSubscription(1, 2);
|
||||
subscription.addClientSubscription(2, 1);
|
||||
subscription.addClientSubscription(2, 2);
|
||||
subscription.deleteClientSubscription(1, 2);
|
||||
subscription.deleteClientSubscription(2, 1);
|
||||
subscription.deleteClientSubscription(2, 2);
|
||||
|
||||
var PLog =require('../src/LiveQuery/PLog');
|
||||
expect(PLog.error).not.toHaveBeenCalled();
|
||||
expect(subscription.clientRequestIds.size).toBe(1);
|
||||
expect(subscription.clientRequestIds.get(1)).toEqual([1]);
|
||||
});
|
||||
|
||||
afterEach(function(){
|
||||
jasmine.restoreLibrary('../src/LiveQuery/PLog', 'error');
|
||||
});
|
||||
});
|
||||
@@ -252,3 +252,22 @@ global.jequal = jequal;
|
||||
global.range = range;
|
||||
global.setServerConfiguration = setServerConfiguration;
|
||||
global.defaultConfiguration = defaultConfiguration;
|
||||
|
||||
// LiveQuery test setting
|
||||
require('../src/LiveQuery/PLog').logLevel = 'NONE';
|
||||
var libraryCache = {};
|
||||
jasmine.mockLibrary = function(library, name, mock) {
|
||||
var original = require(library)[name];
|
||||
if (!libraryCache[library]) {
|
||||
libraryCache[library] = {};
|
||||
}
|
||||
require(library)[name] = mock;
|
||||
libraryCache[library][name] = original;
|
||||
}
|
||||
|
||||
jasmine.restoreLibrary = function(library, name) {
|
||||
if (!libraryCache[library] || !libraryCache[library][name]) {
|
||||
throw 'Can not find library ' + library + ' ' + name;
|
||||
}
|
||||
require(library)[name] = libraryCache[library][name];
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export class Config {
|
||||
this.facebookAppIds = cacheInfo.facebookAppIds;
|
||||
this.allowClientClassCreation = cacheInfo.allowClientClassCreation;
|
||||
this.database = DatabaseAdapter.getDatabaseConnection(applicationId, cacheInfo.collectionPrefix);
|
||||
|
||||
|
||||
this.serverURL = cacheInfo.serverURL;
|
||||
this.publicServerURL = cacheInfo.publicServerURL;
|
||||
this.verifyUserEmails = cacheInfo.verifyUserEmails;
|
||||
@@ -36,14 +36,15 @@ export class Config {
|
||||
this.authDataManager = cacheInfo.authDataManager;
|
||||
this.customPages = cacheInfo.customPages || {};
|
||||
this.mount = mount;
|
||||
this.liveQueryController = cacheInfo.liveQueryController;
|
||||
}
|
||||
|
||||
|
||||
static validate(options) {
|
||||
this.validateEmailConfiguration({verifyUserEmails: options.verifyUserEmails,
|
||||
appName: options.appName,
|
||||
this.validateEmailConfiguration({verifyUserEmails: options.verifyUserEmails,
|
||||
appName: options.appName,
|
||||
publicServerURL: options.publicServerURL})
|
||||
}
|
||||
|
||||
|
||||
static validateEmailConfiguration({verifyUserEmails, appName, publicServerURL}) {
|
||||
if (verifyUserEmails) {
|
||||
if (typeof appName !== 'string') {
|
||||
@@ -58,23 +59,23 @@ export class Config {
|
||||
get invalidLinkURL() {
|
||||
return this.customPages.invalidLink || `${this.publicServerURL}/apps/invalid_link.html`;
|
||||
}
|
||||
|
||||
|
||||
get verifyEmailSuccessURL() {
|
||||
return this.customPages.verifyEmailSuccess || `${this.publicServerURL}/apps/verify_email_success.html`;
|
||||
}
|
||||
|
||||
|
||||
get choosePasswordURL() {
|
||||
return this.customPages.choosePassword || `${this.publicServerURL}/apps/choose_password`;
|
||||
}
|
||||
|
||||
|
||||
get requestResetPasswordURL() {
|
||||
return `${this.publicServerURL}/apps/${this.applicationId}/request_password_reset`;
|
||||
}
|
||||
|
||||
|
||||
get passwordResetSuccessURL() {
|
||||
return this.customPages.passwordResetSuccess || `${this.publicServerURL}/apps/password_reset_success.html`;
|
||||
}
|
||||
|
||||
|
||||
get verifyEmailURL() {
|
||||
return `${this.publicServerURL}/apps/${this.applicationId}/verify_email`;
|
||||
}
|
||||
|
||||
51
src/Controllers/LiveQueryController.js
Normal file
51
src/Controllers/LiveQueryController.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import { ParseCloudCodePublisher } from '../LiveQuery/ParseCloudCodePublisher';
|
||||
|
||||
export class LiveQueryController {
|
||||
classNames: any;
|
||||
liveQueryPublisher: any;
|
||||
|
||||
constructor(config: any) {
|
||||
let classNames;
|
||||
// If config is empty, we just assume no classs needs to be registered as LiveQuery
|
||||
if (!config || !config.classNames) {
|
||||
this.classNames = new Set();
|
||||
} else if (config.classNames instanceof Array) {
|
||||
this.classNames = new Set(config.classNames);
|
||||
} else {
|
||||
throw 'liveQuery.classes should be an array of string'
|
||||
}
|
||||
this.liveQueryPublisher = new ParseCloudCodePublisher(config);
|
||||
}
|
||||
|
||||
onAfterSave(className: string, currentObject: any, originalObject: any) {
|
||||
if (!this.hasLiveQuery(className)) {
|
||||
return;
|
||||
}
|
||||
let req = this._makePublisherRequest(currentObject, originalObject);
|
||||
this.liveQueryPublisher.onCloudCodeAfterSave(req);
|
||||
}
|
||||
|
||||
onAfterDelete(className: string, currentObject: any, originalObject: any) {
|
||||
if (!this.hasLiveQuery(className)) {
|
||||
return;
|
||||
}
|
||||
let req = this._makePublisherRequest(currentObject, originalObject);
|
||||
this.liveQueryPublisher.onCloudCodeAfterDelete(req);
|
||||
}
|
||||
|
||||
hasLiveQuery(className: string): boolean {
|
||||
return this.classNames.has(className);
|
||||
}
|
||||
|
||||
_makePublisherRequest(currentObject: any, originalObject: any): any {
|
||||
let req = {
|
||||
object: currentObject
|
||||
};
|
||||
if (currentObject) {
|
||||
req.original = originalObject;
|
||||
}
|
||||
return req;
|
||||
}
|
||||
}
|
||||
|
||||
export default LiveQueryController;
|
||||
104
src/LiveQuery/Client.js
Normal file
104
src/LiveQuery/Client.js
Normal file
@@ -0,0 +1,104 @@
|
||||
import PLog from './PLog';
|
||||
import Parse from 'parse/node';
|
||||
|
||||
import type { FlattenedObjectData } from './Subscription';
|
||||
export type Message = { [attr: string]: any };
|
||||
|
||||
let dafaultFields = ['className', 'objectId', 'updatedAt', 'createdAt', 'ACL'];
|
||||
|
||||
class Client {
|
||||
id: number;
|
||||
parseWebSocket: any;
|
||||
userId: string;
|
||||
roles: Array<string>;
|
||||
subscriptionInfos: Object;
|
||||
pushConnect: Function;
|
||||
pushSubscribe: Function;
|
||||
pushUnsubscribe: Function;
|
||||
pushCreate: Function;
|
||||
pushEnter: Function;
|
||||
pushUpdate: Function;
|
||||
pushDelete: Function;
|
||||
pushLeave: Function;
|
||||
|
||||
constructor(id: number, parseWebSocket: any) {
|
||||
this.id = id;
|
||||
this.parseWebSocket = parseWebSocket;
|
||||
this.roles = [];
|
||||
this.subscriptionInfos = new Map();
|
||||
this.pushConnect = this._pushEvent('connected');
|
||||
this.pushSubscribe = this._pushEvent('subscribed');
|
||||
this.pushUnsubscribe = this._pushEvent('unsubscribed');
|
||||
this.pushCreate = this._pushEvent('create');
|
||||
this.pushEnter = this._pushEvent('enter');
|
||||
this.pushUpdate = this._pushEvent('update');
|
||||
this.pushDelete = this._pushEvent('delete');
|
||||
this.pushLeave = this._pushEvent('leave');
|
||||
}
|
||||
|
||||
static pushResponse(parseWebSocket: any, message: Message): void {
|
||||
PLog.verbose('Push Response : %j', message);
|
||||
parseWebSocket.send(message);
|
||||
}
|
||||
|
||||
static pushError(parseWebSocket: any, code: number, error: string, reconnect: boolean = true): void {
|
||||
Client.pushResponse(parseWebSocket, JSON.stringify({
|
||||
'op': 'error',
|
||||
'error': error,
|
||||
'code': code,
|
||||
'reconnect': reconnect
|
||||
}));
|
||||
}
|
||||
|
||||
addSubscriptionInfo(requestId: number, subscriptionInfo: any): void {
|
||||
this.subscriptionInfos.set(requestId, subscriptionInfo);
|
||||
}
|
||||
|
||||
getSubscriptionInfo(requestId: numner): any {
|
||||
return this.subscriptionInfos.get(requestId);
|
||||
}
|
||||
|
||||
deleteSubscriptionInfo(requestId: number): void {
|
||||
return this.subscriptionInfos.delete(requestId);
|
||||
}
|
||||
|
||||
_pushEvent(type: string): Function {
|
||||
return function(subscriptionId: number, parseObjectJSON: any): void {
|
||||
let response: Message = {
|
||||
'op' : type,
|
||||
'clientId' : this.id
|
||||
};
|
||||
if (typeof subscriptionId !== 'undefined') {
|
||||
response['requestId'] = subscriptionId;
|
||||
}
|
||||
if (typeof parseObjectJSON !== 'undefined') {
|
||||
let fields;
|
||||
if (this.subscriptionInfos.has(subscriptionId)) {
|
||||
fields = this.subscriptionInfos.get(subscriptionId).fields;
|
||||
}
|
||||
response['object'] = this._toJSONWithFields(parseObjectJSON, fields);
|
||||
}
|
||||
Client.pushResponse(this.parseWebSocket, JSON.stringify(response));
|
||||
}
|
||||
}
|
||||
|
||||
_toJSONWithFields(parseObjectJSON: any, fields: any): FlattenedObjectData {
|
||||
if (!fields) {
|
||||
return parseObjectJSON;
|
||||
}
|
||||
let limitedParseObject = {};
|
||||
for (let field of dafaultFields) {
|
||||
limitedParseObject[field] = parseObjectJSON[field];
|
||||
}
|
||||
for (let field of fields) {
|
||||
if (field in parseObjectJSON) {
|
||||
limitedParseObject[field] = parseObjectJSON[field];
|
||||
}
|
||||
}
|
||||
return limitedParseObject;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
Client
|
||||
}
|
||||
59
src/LiveQuery/EventEmitterPubSub.js
Normal file
59
src/LiveQuery/EventEmitterPubSub.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import events from 'events';
|
||||
|
||||
let emitter = new events.EventEmitter();
|
||||
|
||||
class Publisher {
|
||||
emitter: any;
|
||||
|
||||
constructor(emitter: any) {
|
||||
this.emitter = emitter;
|
||||
}
|
||||
|
||||
publish(channel: string, message: string): void {
|
||||
this.emitter.emit(channel, message);
|
||||
}
|
||||
}
|
||||
|
||||
class Subscriber extends events.EventEmitter {
|
||||
emitter: any;
|
||||
subscriptions: any;
|
||||
|
||||
constructor(emitter: any) {
|
||||
super();
|
||||
this.emitter = emitter;
|
||||
this.subscriptions = new Map();
|
||||
}
|
||||
|
||||
subscribe(channel: string): void {
|
||||
let handler = (message) => {
|
||||
this.emit('message', channel, message);
|
||||
}
|
||||
this.subscriptions.set(channel, handler);
|
||||
this.emitter.on(channel, handler);
|
||||
}
|
||||
|
||||
unsubscribe(channel: string): void {
|
||||
if (!this.subscriptions.has(channel)) {
|
||||
return;
|
||||
}
|
||||
this.emitter.removeListener(channel, this.subscriptions.get(channel));
|
||||
this.subscriptions.delete(channel);
|
||||
}
|
||||
}
|
||||
|
||||
function createPublisher(): any {
|
||||
return new Publisher(emitter);
|
||||
}
|
||||
|
||||
function createSubscriber(): any {
|
||||
return new Subscriber(emitter);
|
||||
}
|
||||
|
||||
let EventEmitterPubSub = {
|
||||
createPublisher,
|
||||
createSubscriber
|
||||
}
|
||||
|
||||
export {
|
||||
EventEmitterPubSub
|
||||
}
|
||||
22
src/LiveQuery/Id.js
Normal file
22
src/LiveQuery/Id.js
Normal file
@@ -0,0 +1,22 @@
|
||||
class Id {
|
||||
className: string;
|
||||
objectId: string;
|
||||
|
||||
constructor(className: string, objectId: string) {
|
||||
this.className = className;
|
||||
this.objectId = objectId;
|
||||
}
|
||||
toString(): string {
|
||||
return this.className + ':' + this.objectId;
|
||||
}
|
||||
|
||||
static fromString(str: string) {
|
||||
var split = str.split(':');
|
||||
if (split.length !== 2) {
|
||||
throw new TypeError('Cannot create Id object from this string');
|
||||
}
|
||||
return new Id(split[0], split[1]);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Id;
|
||||
41
src/LiveQuery/PLog.js
Normal file
41
src/LiveQuery/PLog.js
Normal file
@@ -0,0 +1,41 @@
|
||||
let LogLevel = {
|
||||
'VERBOSE': 0,
|
||||
'DEBUG': 1,
|
||||
'INFO': 2,
|
||||
'ERROR': 3,
|
||||
'NONE': 4
|
||||
}
|
||||
|
||||
function getCurrentLogLevel() {
|
||||
if (PLog.logLevel && PLog.logLevel in LogLevel) {
|
||||
return LogLevel[PLog.logLevel];
|
||||
}
|
||||
return LogLevel['ERROR'];
|
||||
}
|
||||
|
||||
function verbose(): void {
|
||||
if (getCurrentLogLevel() <= LogLevel['VERBOSE']) {
|
||||
console.log.apply(console, arguments)
|
||||
}
|
||||
}
|
||||
|
||||
function log(): void {
|
||||
if (getCurrentLogLevel() <= LogLevel['INFO']) {
|
||||
console.log.apply(console, arguments)
|
||||
}
|
||||
}
|
||||
|
||||
function error(): void {
|
||||
if (getCurrentLogLevel() <= LogLevel['ERROR']) {
|
||||
console.error.apply(console, arguments)
|
||||
}
|
||||
}
|
||||
|
||||
let PLog = {
|
||||
log: log,
|
||||
error: error,
|
||||
verbose: verbose,
|
||||
logLevel: 'INFO'
|
||||
};
|
||||
|
||||
module.exports = PLog;
|
||||
37
src/LiveQuery/ParseCloudCodePublisher.js
Normal file
37
src/LiveQuery/ParseCloudCodePublisher.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { ParsePubSub } from './ParsePubSub';
|
||||
import PLog from './PLog';
|
||||
|
||||
class ParseCloudCodePublisher {
|
||||
parsePublisher: Object;
|
||||
|
||||
// config object of the publisher, right now it only contains the redisURL,
|
||||
// but we may extend it later.
|
||||
constructor(config: any = {}) {
|
||||
this.parsePublisher = ParsePubSub.createPublisher(config);
|
||||
}
|
||||
|
||||
onCloudCodeAfterSave(request: any): void {
|
||||
this._onCloudCodeMessage('afterSave', request);
|
||||
}
|
||||
|
||||
onCloudCodeAfterDelete(request: any): void {
|
||||
this._onCloudCodeMessage('afterDelete', request);
|
||||
}
|
||||
|
||||
// Request is the request object from cloud code functions. request.object is a ParseObject.
|
||||
_onCloudCodeMessage(type: string, request: any): void {
|
||||
PLog.verbose('Raw request from cloud code current : %j | original : %j', request.object, request.original);
|
||||
// We need the full JSON which includes className
|
||||
let message = {
|
||||
currentParseObject: request.object._toFullJSON()
|
||||
}
|
||||
if (request.original) {
|
||||
message.originalParseObject = request.original._toFullJSON();
|
||||
}
|
||||
this.parsePublisher.publish(type, JSON.stringify(message));
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
ParseCloudCodePublisher
|
||||
}
|
||||
460
src/LiveQuery/ParseLiveQueryServer.js
Normal file
460
src/LiveQuery/ParseLiveQueryServer.js
Normal file
@@ -0,0 +1,460 @@
|
||||
import tv4 from 'tv4';
|
||||
import Parse from 'parse/node';
|
||||
import { Subscription } from './Subscription';
|
||||
import { Client } from './Client';
|
||||
import { ParseWebSocketServer } from './ParseWebSocketServer';
|
||||
import PLog from './PLog';
|
||||
import RequestSchema from './RequestSchema';
|
||||
import { matchesQuery, queryHash } from './QueryTools';
|
||||
import { ParsePubSub } from './ParsePubSub';
|
||||
import { SessionTokenCache } from './SessionTokenCache';
|
||||
|
||||
class ParseLiveQueryServer {
|
||||
clientId: number;
|
||||
clients: Object;
|
||||
// className -> (queryHash -> subscription)
|
||||
subscriptions: Object;
|
||||
parseWebSocketServer: Object;
|
||||
keyPairs : any;
|
||||
// The subscriber we use to get object update from publisher
|
||||
subscriber: Object;
|
||||
|
||||
constructor(server: any, config: any) {
|
||||
this.clientId = 0;
|
||||
this.clients = new Map();
|
||||
this.subscriptions = new Map();
|
||||
|
||||
config = config || {};
|
||||
// Set LogLevel
|
||||
PLog.logLevel = config.logLevel || 'INFO';
|
||||
|
||||
// Store keys, convert obj to map
|
||||
let keyPairs = config.keyPairs || {};
|
||||
this.keyPairs = new Map();
|
||||
for (let key of Object.keys(keyPairs)) {
|
||||
this.keyPairs.set(key, keyPairs[key]);
|
||||
}
|
||||
PLog.verbose('Support key pairs', this.keyPairs);
|
||||
|
||||
// Initialize Parse
|
||||
Parse.Object.disableSingleInstance();
|
||||
Parse.User.enableUnsafeCurrentUser();
|
||||
|
||||
let serverURL = config.serverURL || Parse.serverURL;
|
||||
Parse.serverURL = serverURL;
|
||||
let appId = config.appId || Parse.applicationId;
|
||||
let javascriptKey = Parse.javaScriptKey;
|
||||
let masterKey = config.masterKey || Parse.masterKey;
|
||||
Parse.initialize(appId, javascriptKey, masterKey);
|
||||
|
||||
// Initialize websocket server
|
||||
this.parseWebSocketServer = new ParseWebSocketServer(
|
||||
server,
|
||||
(parseWebsocket) => this._onConnect(parseWebsocket),
|
||||
config.websocketTimeout
|
||||
);
|
||||
|
||||
// Initialize subscriber
|
||||
this.subscriber = ParsePubSub.createSubscriber({
|
||||
redisURL: config.redisURL
|
||||
});
|
||||
this.subscriber.subscribe('afterSave');
|
||||
this.subscriber.subscribe('afterDelete');
|
||||
// Register message handler for subscriber. When publisher get messages, it will publish message
|
||||
// to the subscribers and the handler will be called.
|
||||
this.subscriber.on('message', (channel, messageStr) => {
|
||||
PLog.verbose('Subscribe messsage %j', messageStr);
|
||||
let message = JSON.parse(messageStr);
|
||||
this._inflateParseObject(message);
|
||||
if (channel === 'afterSave') {
|
||||
this._onAfterSave(message);
|
||||
} else if (channel === 'afterDelete') {
|
||||
this._onAfterDelete(message);
|
||||
} else {
|
||||
PLog.error('Get message %s from unknown channel %j', message, channel);
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize sessionToken cache
|
||||
this.sessionTokenCache = new SessionTokenCache(config.cacheTimeout);
|
||||
}
|
||||
|
||||
// Message is the JSON object from publisher. Message.currentParseObject is the ParseObject JSON after changes.
|
||||
// Message.originalParseObject is the original ParseObject JSON.
|
||||
_inflateParseObject(message: any): void {
|
||||
// Inflate merged object
|
||||
let currentParseObject = message.currentParseObject;
|
||||
let className = currentParseObject.className;
|
||||
let parseObject = new Parse.Object(className);
|
||||
parseObject._finishFetch(currentParseObject);
|
||||
message.currentParseObject = parseObject;
|
||||
// Inflate original object
|
||||
let originalParseObject = message.originalParseObject;
|
||||
if (originalParseObject) {
|
||||
className = originalParseObject.className;
|
||||
parseObject = new Parse.Object(className);
|
||||
parseObject._finishFetch(originalParseObject);
|
||||
message.originalParseObject = parseObject;
|
||||
}
|
||||
}
|
||||
|
||||
// Message is the JSON object from publisher after inflated. Message.currentParseObject is the ParseObject after changes.
|
||||
// Message.originalParseObject is the original ParseObject.
|
||||
_onAfterDelete(message: any): void {
|
||||
PLog.verbose('afterDelete is triggered');
|
||||
|
||||
let deletedParseObject = message.currentParseObject.toJSON();
|
||||
let className = deletedParseObject.className;
|
||||
PLog.verbose('ClassName: %j | ObjectId: %s', className, deletedParseObject.id);
|
||||
PLog.verbose('Current client number : %d', this.clients.size);
|
||||
|
||||
let classSubscriptions = this.subscriptions.get(className);
|
||||
if (typeof classSubscriptions === 'undefined') {
|
||||
PLog.error('Can not find subscriptions under this class ' + className);
|
||||
return;
|
||||
}
|
||||
for (let subscription of classSubscriptions.values()) {
|
||||
let isSubscriptionMatched = this._matchesSubscription(deletedParseObject, subscription);
|
||||
if (!isSubscriptionMatched) {
|
||||
continue;
|
||||
}
|
||||
for (let [clientId, requestIds] of subscription.clientRequestIds.entries()) {
|
||||
let client = this.clients.get(clientId);
|
||||
if (typeof client === 'undefined') {
|
||||
continue;
|
||||
}
|
||||
for (let requestId of requestIds) {
|
||||
let acl = message.currentParseObject.getACL();
|
||||
// Check ACL
|
||||
this._matchesACL(acl, client, requestId).then((isMatched) => {
|
||||
if (!isMatched) {
|
||||
return null;
|
||||
}
|
||||
client.pushDelete(requestId, deletedParseObject);
|
||||
}, (error) => {
|
||||
PLog.error('Matching ACL error : ', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Message is the JSON object from publisher after inflated. Message.currentParseObject is the ParseObject after changes.
|
||||
// Message.originalParseObject is the original ParseObject.
|
||||
_onAfterSave(message: any): void {
|
||||
PLog.verbose('afterSave is triggered');
|
||||
|
||||
let originalParseObject = null;
|
||||
if (message.originalParseObject) {
|
||||
originalParseObject = message.originalParseObject.toJSON();
|
||||
}
|
||||
let currentParseObject = message.currentParseObject.toJSON();
|
||||
let className = currentParseObject.className;
|
||||
PLog.verbose('ClassName: %s | ObjectId: %s', className, currentParseObject.id);
|
||||
PLog.verbose('Current client number : %d', this.clients.size);
|
||||
|
||||
let classSubscriptions = this.subscriptions.get(className);
|
||||
if (typeof classSubscriptions === 'undefined') {
|
||||
PLog.error('Can not find subscriptions under this class ' + className);
|
||||
return;
|
||||
}
|
||||
for (let subscription of classSubscriptions.values()) {
|
||||
let isOriginalSubscriptionMatched = this._matchesSubscription(originalParseObject, subscription);
|
||||
let isCurrentSubscriptionMatched = this._matchesSubscription(currentParseObject, subscription);
|
||||
for (let [clientId, requestIds] of subscription.clientRequestIds.entries()) {
|
||||
let client = this.clients.get(clientId);
|
||||
if (typeof client === 'undefined') {
|
||||
continue;
|
||||
}
|
||||
for (let requestId of requestIds) {
|
||||
// Set orignal ParseObject ACL checking promise, if the object does not match
|
||||
// subscription, we do not need to check ACL
|
||||
let originalACLCheckingPromise;
|
||||
if (!isOriginalSubscriptionMatched) {
|
||||
originalACLCheckingPromise = Parse.Promise.as(false);
|
||||
} else {
|
||||
let originalACL;
|
||||
if (message.originalParseObject) {
|
||||
originalACL = message.originalParseObject.getACL();
|
||||
}
|
||||
originalACLCheckingPromise = this._matchesACL(originalACL, client, requestId);
|
||||
}
|
||||
// Set current ParseObject ACL checking promise, if the object does not match
|
||||
// subscription, we do not need to check ACL
|
||||
let currentACLCheckingPromise;
|
||||
if (!isCurrentSubscriptionMatched) {
|
||||
currentACLCheckingPromise = Parse.Promise.as(false);
|
||||
} else {
|
||||
let currentACL = message.currentParseObject.getACL();
|
||||
currentACLCheckingPromise = this._matchesACL(currentACL, client, requestId);
|
||||
}
|
||||
|
||||
Parse.Promise.when(
|
||||
originalACLCheckingPromise,
|
||||
currentACLCheckingPromise
|
||||
).then((isOriginalMatched, isCurrentMatched) => {
|
||||
PLog.verbose('Original %j | Current %j | Match: %s, %s, %s, %s | Query: %s',
|
||||
originalParseObject,
|
||||
currentParseObject,
|
||||
isOriginalSubscriptionMatched,
|
||||
isCurrentSubscriptionMatched,
|
||||
isOriginalMatched,
|
||||
isCurrentMatched,
|
||||
subscription.hash
|
||||
);
|
||||
|
||||
// Decide event type
|
||||
let type;
|
||||
if (isOriginalMatched && isCurrentMatched) {
|
||||
type = 'Update';
|
||||
} else if (isOriginalMatched && !isCurrentMatched) {
|
||||
type = 'Leave';
|
||||
} else if (!isOriginalMatched && isCurrentMatched) {
|
||||
if (originalParseObject) {
|
||||
type = 'Enter';
|
||||
} else {
|
||||
type = 'Create';
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
let functionName = 'push' + type;
|
||||
client[functionName](requestId, currentParseObject);
|
||||
}, (error) => {
|
||||
PLog.error('Matching ACL error : ', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onConnect(parseWebsocket: any): void {
|
||||
parseWebsocket.on('message', (request) => {
|
||||
if (typeof request === 'string') {
|
||||
request = JSON.parse(request);
|
||||
}
|
||||
PLog.verbose('Request: %j', request);
|
||||
|
||||
// Check whether this request is a valid request, return error directly if not
|
||||
if (!tv4.validate(request, RequestSchema['general']) || !tv4.validate(request, RequestSchema[request.op])) {
|
||||
Client.pushError(parseWebsocket, 1, tv4.error.message);
|
||||
PLog.error('Connect message error %s', tv4.error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
switch(request.op) {
|
||||
case 'connect':
|
||||
this._handleConnect(parseWebsocket, request);
|
||||
break;
|
||||
case 'subscribe':
|
||||
this._handleSubscribe(parseWebsocket, request);
|
||||
break;
|
||||
case 'unsubscribe':
|
||||
this._handleUnsubscribe(parseWebsocket, request);
|
||||
break;
|
||||
default:
|
||||
Client.pushError(parseWebsocket, 3, 'Get unknown operation');
|
||||
PLog.error('Get unknown operation', request.op);
|
||||
}
|
||||
});
|
||||
|
||||
parseWebsocket.on('disconnect', () => {
|
||||
PLog.log('Client disconnect: %d', parseWebsocket.clientId);
|
||||
let clientId = parseWebsocket.clientId;
|
||||
if (!this.clients.has(clientId)) {
|
||||
PLog.error('Can not find client %d on disconnect', clientId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete client
|
||||
let client = this.clients.get(clientId);
|
||||
this.clients.delete(clientId);
|
||||
|
||||
// Delete client from subscriptions
|
||||
for (let [requestId, subscriptionInfo] of client.subscriptionInfos.entries()) {
|
||||
let subscription = subscriptionInfo.subscription;
|
||||
subscription.deleteClientSubscription(clientId, requestId);
|
||||
|
||||
// If there is no client which is subscribing this subscription, remove it from subscriptions
|
||||
let classSubscriptions = this.subscriptions.get(subscription.className);
|
||||
if (!subscription.hasSubscribingClient()) {
|
||||
classSubscriptions.delete(subscription.hash);
|
||||
}
|
||||
// If there is no subscriptions under this class, remove it from subscriptions
|
||||
if (classSubscriptions.size === 0) {
|
||||
this.subscriptions.delete(subscription.className);
|
||||
}
|
||||
}
|
||||
|
||||
PLog.verbose('Current clients %d', this.clients.size);
|
||||
PLog.verbose('Current subscriptions %d', this.subscriptions.size);
|
||||
});
|
||||
}
|
||||
|
||||
_matchesSubscription(parseObject: any, subscription: any): boolean {
|
||||
// Object is undefined or null, not match
|
||||
if (!parseObject) {
|
||||
return false;
|
||||
}
|
||||
return matchesQuery(parseObject, subscription.query);
|
||||
}
|
||||
|
||||
_matchesACL(acl: any, client: any, requestId: number): any {
|
||||
// If ACL is undefined or null, or ACL has public read access, return true directly
|
||||
if (!acl || acl.getPublicReadAccess()) {
|
||||
return Parse.Promise.as(true);
|
||||
}
|
||||
// Check subscription sessionToken matches ACL first
|
||||
let subscriptionInfo = client.getSubscriptionInfo(requestId);
|
||||
if (typeof subscriptionInfo === 'undefined') {
|
||||
return Parse.Promise.as(false);
|
||||
}
|
||||
|
||||
let subscriptionSessionToken = subscriptionInfo.sessionToken;
|
||||
return this.sessionTokenCache.getUserId(subscriptionSessionToken).then((userId) => {
|
||||
return acl.getReadAccess(userId);
|
||||
}).then((isSubscriptionSessionTokenMatched) => {
|
||||
if (isSubscriptionSessionTokenMatched) {
|
||||
return Parse.Promise.as(true);
|
||||
}
|
||||
// Check client sessionToken matches ACL
|
||||
let clientSessionToken = client.sessionToken;
|
||||
return this.sessionTokenCache.getUserId(clientSessionToken).then((userId) => {
|
||||
return acl.getReadAccess(userId);
|
||||
});
|
||||
}).then((isMatched) => {
|
||||
return Parse.Promise.as(isMatched);
|
||||
}, (error) => {
|
||||
return Parse.Promise.as(false);
|
||||
});
|
||||
}
|
||||
|
||||
_handleConnect(parseWebsocket: any, request: any): any {
|
||||
if (!this._validateKeys(request, this.keyPairs)) {
|
||||
Client.pushError(parseWebsocket, 4, 'Key in request is not valid');
|
||||
PLog.error('Key in request is not valid');
|
||||
return;
|
||||
}
|
||||
let client = new Client(this.clientId, parseWebsocket);
|
||||
parseWebsocket.clientId = this.clientId;
|
||||
this.clientId += 1;
|
||||
this.clients.set(parseWebsocket.clientId, client);
|
||||
PLog.log('Create new client: %d', parseWebsocket.clientId);
|
||||
client.pushConnect();
|
||||
}
|
||||
|
||||
_validateKeys(request: any, validKeyPairs: any): boolean {
|
||||
if (!validKeyPairs || validKeyPairs.size == 0) {
|
||||
return true;
|
||||
}
|
||||
let isValid = false;
|
||||
for (let [key, secret] of validKeyPairs) {
|
||||
if (!request[key] || request[key] !== secret) {
|
||||
continue;
|
||||
}
|
||||
isValid = true;
|
||||
break;
|
||||
}
|
||||
return isValid;
|
||||
}
|
||||
|
||||
_handleSubscribe(parseWebsocket: any, request: any): 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 subscribing');
|
||||
PLog.error('Can not find this client, make sure you connect to server before subscribing');
|
||||
return;
|
||||
}
|
||||
let client = this.clients.get(parseWebsocket.clientId);
|
||||
|
||||
// Get subscription from subscriptions, create one if necessary
|
||||
let subscriptionHash = queryHash(request.query);
|
||||
// Add className to subscriptions if necessary
|
||||
let className = request.query.className;
|
||||
if (!this.subscriptions.has(className)) {
|
||||
this.subscriptions.set(className, new Map());
|
||||
}
|
||||
let classSubscriptions = this.subscriptions.get(className);
|
||||
let subscription;
|
||||
if (classSubscriptions.has(subscriptionHash)) {
|
||||
subscription = classSubscriptions.get(subscriptionHash);
|
||||
} else {
|
||||
subscription = new Subscription(className, request.query.where, subscriptionHash);
|
||||
classSubscriptions.set(subscriptionHash, subscription);
|
||||
}
|
||||
|
||||
// Add subscriptionInfo to client
|
||||
let subscriptionInfo = {
|
||||
subscription: subscription
|
||||
};
|
||||
// Add selected fields and sessionToken for this subscription if necessary
|
||||
if (request.query.fields) {
|
||||
subscriptionInfo.fields = request.query.fields;
|
||||
}
|
||||
if (request.sessionToken) {
|
||||
subscriptionInfo.sessionToken = request.sessionToken;
|
||||
}
|
||||
client.addSubscriptionInfo(request.requestId, subscriptionInfo);
|
||||
|
||||
// Add clientId to subscription
|
||||
subscription.addClientSubscription(parseWebsocket.clientId, request.requestId);
|
||||
|
||||
client.pushSubscribe(request.requestId);
|
||||
|
||||
PLog.verbose('Create client %d new subscription: %d', parseWebsocket.clientId, request.requestId);
|
||||
PLog.verbose('Current client number: %d', this.clients.size);
|
||||
}
|
||||
|
||||
_handleUnsubscribe(parseWebsocket: any, request: any): 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');
|
||||
PLog.error('Can not find this client, make sure you connect to server before unsubscribing');
|
||||
return;
|
||||
}
|
||||
let requestId = request.requestId;
|
||||
let client = this.clients.get(parseWebsocket.clientId);
|
||||
if (typeof client === 'undefined') {
|
||||
Client.pushError(parseWebsocket, 2, 'Cannot find client with clientId ' + parseWebsocket.clientId +
|
||||
'. Make sure you connect to live query server before unsubscribing.');
|
||||
PLog.error('Can not find this client ' + parseWebsocket.clientId);
|
||||
return;
|
||||
}
|
||||
|
||||
let subscriptionInfo = client.getSubscriptionInfo(requestId);
|
||||
if (typeof subscriptionInfo === 'undefined') {
|
||||
Client.pushError(parseWebsocket, 2, 'Cannot find subscription with clientId ' + parseWebsocket.clientId +
|
||||
' subscriptionId ' + requestId + '. Make sure you subscribe to live query server before unsubscribing.');
|
||||
PLog.error('Can not find subscription with clientId ' + parseWebsocket.clientId + ' subscriptionId ' + requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove subscription from client
|
||||
client.deleteSubscriptionInfo(requestId);
|
||||
// Remove client from subscription
|
||||
let subscription = subscriptionInfo.subscription;
|
||||
let className = subscription.className;
|
||||
subscription.deleteClientSubscription(parseWebsocket.clientId, requestId);
|
||||
// If there is no client which is subscribing this subscription, remove it from subscriptions
|
||||
let classSubscriptions = this.subscriptions.get(className);
|
||||
if (!subscription.hasSubscribingClient()) {
|
||||
classSubscriptions.delete(subscription.hash);
|
||||
}
|
||||
// If there is no subscriptions under this class, remove it from subscriptions
|
||||
if (classSubscriptions.size === 0) {
|
||||
this.subscriptions.delete(className);
|
||||
}
|
||||
|
||||
client.pushUnsubscribe(request.requestId);
|
||||
|
||||
PLog.verbose('Delete client: %d | subscription: %d', parseWebsocket.clientId, request.requestId);
|
||||
}
|
||||
}
|
||||
|
||||
ParseLiveQueryServer.setLogLevel = function(logLevel) {
|
||||
PLog.logLevel = logLevel;
|
||||
}
|
||||
|
||||
export {
|
||||
ParseLiveQueryServer
|
||||
}
|
||||
29
src/LiveQuery/ParsePubSub.js
Normal file
29
src/LiveQuery/ParsePubSub.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { RedisPubSub } from './RedisPubSub';
|
||||
import { EventEmitterPubSub } from './EventEmitterPubSub';
|
||||
|
||||
let ParsePubSub = {};
|
||||
|
||||
function useRedis(config: any): boolean {
|
||||
let redisURL = config.redisURL;
|
||||
return typeof redisURL !== 'undefined' && redisURL !== '';
|
||||
}
|
||||
|
||||
ParsePubSub.createPublisher = function(config: any): any {
|
||||
if (useRedis(config)) {
|
||||
return RedisPubSub.createPublisher(config.redisURL);
|
||||
} else {
|
||||
return EventEmitterPubSub.createPublisher();
|
||||
}
|
||||
}
|
||||
|
||||
ParsePubSub.createSubscriber = function(config: any): void {
|
||||
if (useRedis(config)) {
|
||||
return RedisPubSub.createSubscriber(config.redisURL);
|
||||
} else {
|
||||
return EventEmitterPubSub.createSubscriber();
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
ParsePubSub
|
||||
}
|
||||
44
src/LiveQuery/ParseWebSocketServer.js
Normal file
44
src/LiveQuery/ParseWebSocketServer.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import PLog from './PLog';
|
||||
|
||||
let typeMap = new Map([['disconnect', 'close']]);
|
||||
|
||||
export class ParseWebSocketServer {
|
||||
server: Object;
|
||||
|
||||
constructor(server: any, onConnect: Function, websocketTimeout: number = 10 * 1000) {
|
||||
let WebSocketServer = require('ws').Server;
|
||||
let wss = new WebSocketServer({ server: server });
|
||||
wss.on('listening', () => {
|
||||
PLog.log('Parse LiveQuery Server starts running');
|
||||
});
|
||||
wss.on('connection', (ws) => {
|
||||
onConnect(new ParseWebSocket(ws));
|
||||
// Send ping to client periodically
|
||||
let pingIntervalId = setInterval(() => {
|
||||
if (ws.readyState == ws.OPEN) {
|
||||
ws.ping();
|
||||
} else {
|
||||
clearInterval(pingIntervalId);
|
||||
}
|
||||
}, websocketTimeout);
|
||||
});
|
||||
this.server = wss;
|
||||
}
|
||||
}
|
||||
|
||||
export class ParseWebSocket {
|
||||
ws: any;
|
||||
|
||||
constructor(ws: any) {
|
||||
this.ws = ws;
|
||||
}
|
||||
|
||||
on(type: string, callback): void {
|
||||
let wsType = typeMap.has(type) ? typeMap.get(type) : type;
|
||||
this.ws.on(wsType, callback);
|
||||
}
|
||||
|
||||
send(message: any, channel: string): void {
|
||||
this.ws.send(message);
|
||||
}
|
||||
}
|
||||
280
src/LiveQuery/QueryTools.js
Normal file
280
src/LiveQuery/QueryTools.js
Normal file
@@ -0,0 +1,280 @@
|
||||
var equalObjects = require('./equalObjects');
|
||||
var Id = require('./Id');
|
||||
var Parse = require('parse/node');
|
||||
|
||||
/**
|
||||
* Query Hashes are deterministic hashes for Parse Queries.
|
||||
* Any two queries that have the same set of constraints will produce the same
|
||||
* hash. This lets us reliably group components by the queries they depend upon,
|
||||
* and quickly determine if a query has changed.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Convert $or queries into an array of where conditions
|
||||
*/
|
||||
function flattenOrQueries(where) {
|
||||
if (!where.hasOwnProperty('$or')) {
|
||||
return where;
|
||||
}
|
||||
var accum = [];
|
||||
for (var i = 0; i < where.$or.length; i++) {
|
||||
accum = accum.concat(where.$or[i]);
|
||||
}
|
||||
return accum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deterministically turns an object into a string. Disregards ordering
|
||||
*/
|
||||
function stringify(object): string {
|
||||
if (typeof object !== 'object' || object === null) {
|
||||
if (typeof object === 'string') {
|
||||
return '"' + object.replace(/\|/g, '%|') + '"';
|
||||
}
|
||||
return object + '';
|
||||
}
|
||||
if (Array.isArray(object)) {
|
||||
var copy = object.map(stringify);
|
||||
copy.sort();
|
||||
return '[' + copy.join(',') + ']';
|
||||
}
|
||||
var sections = [];
|
||||
var keys = Object.keys(object);
|
||||
keys.sort();
|
||||
for (var k = 0; k < keys.length; k++) {
|
||||
sections.push(stringify(keys[k]) + ':' + stringify(object[keys[k]]));
|
||||
}
|
||||
return '{' + sections.join(',') + '}';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a hash from a query, with unique fields for columns, values, order,
|
||||
* skip, and limit.
|
||||
*/
|
||||
function queryHash(query) {
|
||||
if (query instanceof Parse.Query) {
|
||||
query = {
|
||||
className: query.className,
|
||||
where: query._where
|
||||
}
|
||||
}
|
||||
var where = flattenOrQueries(query.where || {});
|
||||
var columns = [];
|
||||
var values = [];
|
||||
var i;
|
||||
if (Array.isArray(where)) {
|
||||
var uniqueColumns = {};
|
||||
for (i = 0; i < where.length; i++) {
|
||||
var subValues = {};
|
||||
var keys = Object.keys(where[i]);
|
||||
keys.sort();
|
||||
for (var j = 0; j < keys.length; j++) {
|
||||
subValues[keys[j]] = where[i][keys[j]];
|
||||
uniqueColumns[keys[j]] = true;
|
||||
}
|
||||
values.push(subValues);
|
||||
}
|
||||
columns = Object.keys(uniqueColumns);
|
||||
columns.sort();
|
||||
} else {
|
||||
columns = Object.keys(where);
|
||||
columns.sort();
|
||||
for (i = 0; i < columns.length; i++) {
|
||||
values.push(where[columns[i]]);
|
||||
}
|
||||
}
|
||||
|
||||
var sections = [columns.join(','), stringify(values)];
|
||||
|
||||
return query.className + ':' + sections.join('|');
|
||||
}
|
||||
|
||||
/**
|
||||
* matchesQuery -- Determines if an object would be returned by a Parse Query
|
||||
* It's a lightweight, where-clause only implementation of a full query engine.
|
||||
* Since we find queries that match objects, rather than objects that match
|
||||
* queries, we can avoid building a full-blown query tool.
|
||||
*/
|
||||
function matchesQuery(object: any, query: any): boolean {
|
||||
if (query instanceof Parse.Query) {
|
||||
var className =
|
||||
(object.id instanceof Id) ? object.id.className : object.className;
|
||||
if (className !== query.className) {
|
||||
return false;
|
||||
}
|
||||
return matchesQuery(object, query._where);
|
||||
}
|
||||
for (var field in query) {
|
||||
if (!matchesKeyConstraints(object, field, query[field])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determines whether an object matches a single key's constraints
|
||||
*/
|
||||
function matchesKeyConstraints(object, key, constraints) {
|
||||
var i;
|
||||
if (key === '$or') {
|
||||
for (i = 0; i < constraints.length; i++) {
|
||||
if (matchesQuery(object, constraints[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (key === '$relatedTo') {
|
||||
// Bail! We can't handle relational queries locally
|
||||
return false;
|
||||
}
|
||||
// Equality (or Array contains) cases
|
||||
if (typeof constraints !== 'object') {
|
||||
if (Array.isArray(object[key])) {
|
||||
return object[key].indexOf(constraints) > -1;
|
||||
}
|
||||
return object[key] === constraints;
|
||||
}
|
||||
var compareTo;
|
||||
if (constraints.__type) {
|
||||
if (constraints.__type === 'Pointer') {
|
||||
return (
|
||||
constraints.className === object[key].className &&
|
||||
constraints.objectId === object[key].objectId
|
||||
);
|
||||
}
|
||||
compareTo = Parse._decode(key, constraints);
|
||||
if (Array.isArray(object[key])) {
|
||||
for (i = 0; i < object[key].length; i++) {
|
||||
if (equalObjects(object[key][i], compareTo)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return equalObjects(object[key], compareTo);
|
||||
}
|
||||
// More complex cases
|
||||
for (var condition in constraints) {
|
||||
compareTo = constraints[condition];
|
||||
if (compareTo.__type) {
|
||||
compareTo = Parse._decode(key, compareTo);
|
||||
}
|
||||
switch (condition) {
|
||||
case '$lt':
|
||||
if (object[key] >= compareTo) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '$lte':
|
||||
if (object[key] > compareTo) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '$gt':
|
||||
if (object[key] <= compareTo) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '$gte':
|
||||
if (object[key] < compareTo) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '$ne':
|
||||
if (equalObjects(object[key], compareTo)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '$in':
|
||||
if (compareTo.indexOf(object[key]) < 0) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '$nin':
|
||||
if (compareTo.indexOf(object[key]) > -1) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '$all':
|
||||
for (i = 0; i < compareTo.length; i++) {
|
||||
if (object[key].indexOf(compareTo[i]) < 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case '$exists':
|
||||
if (typeof object[key] === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '$regex':
|
||||
if (typeof compareTo === 'object') {
|
||||
return compareTo.test(object[key]);
|
||||
}
|
||||
// JS doesn't support perl-style escaping
|
||||
var expString = '';
|
||||
var escapeEnd = -2;
|
||||
var escapeStart = compareTo.indexOf('\\Q');
|
||||
while (escapeStart > -1) {
|
||||
// Add the unescaped portion
|
||||
expString += compareTo.substring(escapeEnd + 2, escapeStart);
|
||||
escapeEnd = compareTo.indexOf('\\E', escapeStart);
|
||||
if (escapeEnd > -1) {
|
||||
expString += compareTo.substring(escapeStart + 2, escapeEnd)
|
||||
.replace(/\\\\\\\\E/g, '\\E').replace(/\W/g, '\\$&');
|
||||
}
|
||||
|
||||
escapeStart = compareTo.indexOf('\\Q', escapeEnd);
|
||||
}
|
||||
expString += compareTo.substring(Math.max(escapeStart, escapeEnd + 2));
|
||||
var exp = new RegExp(expString, constraints.$options || '');
|
||||
if (!exp.test(object[key])) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '$nearSphere':
|
||||
var distance = compareTo.radiansTo(object[key]);
|
||||
var max = constraints.$maxDistance || Infinity;
|
||||
return distance <= max;
|
||||
case '$within':
|
||||
var southWest = compareTo.$box[0];
|
||||
var northEast = compareTo.$box[1];
|
||||
if (southWest.latitude > northEast.latitude ||
|
||||
southWest.longitude > northEast.longitude) {
|
||||
// Invalid box, crosses the date line
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
object[key].latitude > southWest.latitude &&
|
||||
object[key].latitude < northEast.latitude &&
|
||||
object[key].longitude > southWest.longitude &&
|
||||
object[key].longitude < northEast.longitude
|
||||
);
|
||||
case '$options':
|
||||
// Not a query type, but a way to add options to $regex. Ignore and
|
||||
// avoid the default
|
||||
break;
|
||||
case '$maxDistance':
|
||||
// Not a query type, but a way to add a cap to $nearSphere. Ignore and
|
||||
// avoid the default
|
||||
break;
|
||||
case '$select':
|
||||
return false;
|
||||
case '$dontSelect':
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
var QueryTools = {
|
||||
queryHash: queryHash,
|
||||
matchesQuery: matchesQuery
|
||||
};
|
||||
|
||||
module.exports = QueryTools;
|
||||
18
src/LiveQuery/RedisPubSub.js
Normal file
18
src/LiveQuery/RedisPubSub.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import redis from 'redis';
|
||||
|
||||
function createPublisher(redisURL: string): any {
|
||||
return redis.createClient(redisURL, { no_ready_check: true });
|
||||
}
|
||||
|
||||
function createSubscriber(redisURL: string): any {
|
||||
return redis.createClient(redisURL, { no_ready_check: true });
|
||||
}
|
||||
|
||||
let RedisPubSub = {
|
||||
createPublisher,
|
||||
createSubscriber
|
||||
}
|
||||
|
||||
export {
|
||||
RedisPubSub
|
||||
}
|
||||
101
src/LiveQuery/RequestSchema.js
Normal file
101
src/LiveQuery/RequestSchema.js
Normal file
@@ -0,0 +1,101 @@
|
||||
let general = {
|
||||
'title': 'General request schema',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'op': {
|
||||
'type': 'string',
|
||||
'enum': ['connect', 'subscribe', 'unsubscribe']
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let connect = {
|
||||
'title': 'Connect operation schema',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'op': 'connect',
|
||||
'applicationId': {
|
||||
'type': 'string'
|
||||
},
|
||||
'javascriptKey': {
|
||||
type: 'string'
|
||||
},
|
||||
'masterKey': {
|
||||
type: 'string'
|
||||
},
|
||||
'clientKey': {
|
||||
type: 'string'
|
||||
},
|
||||
'windowsKey': {
|
||||
type: 'string'
|
||||
},
|
||||
'restAPIKey': {
|
||||
'type': 'string'
|
||||
},
|
||||
'sessionToken': {
|
||||
'type': 'string'
|
||||
}
|
||||
},
|
||||
'required': ['op', 'applicationId'],
|
||||
"additionalProperties": false
|
||||
};
|
||||
|
||||
let subscribe = {
|
||||
'title': 'Subscribe operation schema',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'op': 'subscribe',
|
||||
'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',
|
||||
'properties': {
|
||||
'op': 'unsubscribe',
|
||||
'requestId': {
|
||||
'type': 'number'
|
||||
}
|
||||
},
|
||||
'required': ['op', 'requestId'],
|
||||
"additionalProperties": false
|
||||
}
|
||||
|
||||
let RequestSchema = {
|
||||
'general': general,
|
||||
'connect': connect,
|
||||
'subscribe': subscribe,
|
||||
'unsubscribe': unsubscribe
|
||||
}
|
||||
|
||||
export default RequestSchema;
|
||||
38
src/LiveQuery/SessionTokenCache.js
Normal file
38
src/LiveQuery/SessionTokenCache.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import Parse from 'parse/node';
|
||||
import LRU from 'lru-cache';
|
||||
import PLog from './PLog';
|
||||
|
||||
class SessionTokenCache {
|
||||
cache: Object;
|
||||
|
||||
constructor(timeout: number = 30 * 24 * 60 *60 * 1000, maxSize: number = 10000) {
|
||||
this.cache = new LRU({
|
||||
max: maxSize,
|
||||
maxAge: timeout
|
||||
});
|
||||
}
|
||||
|
||||
getUserId(sessionToken: string): any {
|
||||
if (!sessionToken) {
|
||||
return Parse.Promise.error('Empty sessionToken');
|
||||
}
|
||||
let userId = this.cache.get(sessionToken);
|
||||
if (userId) {
|
||||
PLog.verbose('Fetch userId %s of sessionToken %s from Cache', userId, sessionToken);
|
||||
return Parse.Promise.as(userId);
|
||||
}
|
||||
return Parse.User.become(sessionToken).then((user) => {
|
||||
PLog.verbose('Fetch userId %s of sessionToken %s from Parse', user.id, sessionToken);
|
||||
let userId = user.id;
|
||||
this.cache.set(sessionToken, userId);
|
||||
return Parse.Promise.as(userId);
|
||||
}, (error) => {
|
||||
PLog.error('Can not fetch userId for sessionToken %j, error %j', sessionToken, error);
|
||||
return Parse.Promise.error(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
SessionTokenCache
|
||||
}
|
||||
55
src/LiveQuery/Subscription.js
Normal file
55
src/LiveQuery/Subscription.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import {matchesQuery, queryHash} from './QueryTools';
|
||||
import PLog from './PLog';
|
||||
|
||||
export type FlattenedObjectData = { [attr: string]: any };
|
||||
export type QueryData = { [attr: string]: any };
|
||||
|
||||
class Subscription {
|
||||
// It is query condition eg query.where
|
||||
query: QueryData;
|
||||
className: string;
|
||||
hash: string;
|
||||
clientRequestIds: Object;
|
||||
|
||||
constructor(className: string, query: QueryData, queryHash: string) {
|
||||
this.className = className;
|
||||
this.query = query;
|
||||
this.hash = queryHash;
|
||||
this.clientRequestIds = new Map();
|
||||
}
|
||||
|
||||
addClientSubscription(clientId: number, requestId: number): void {
|
||||
if (!this.clientRequestIds.has(clientId)) {
|
||||
this.clientRequestIds.set(clientId, []);
|
||||
}
|
||||
let requestIds = this.clientRequestIds.get(clientId);
|
||||
requestIds.push(requestId);
|
||||
}
|
||||
|
||||
deleteClientSubscription(clientId: number, requestId: number): void {
|
||||
let requestIds = this.clientRequestIds.get(clientId);
|
||||
if (typeof requestIds === 'undefined') {
|
||||
PLog.error('Can not find client %d to delete', clientId);
|
||||
return;
|
||||
}
|
||||
|
||||
let index = requestIds.indexOf(requestId);
|
||||
if (index < 0) {
|
||||
PLog.error('Can not find client %d subscription %d to delete', clientId, requestId);
|
||||
return;
|
||||
}
|
||||
requestIds.splice(index, 1);
|
||||
// Delete client reference if it has no subscription
|
||||
if (requestIds.length == 0) {
|
||||
this.clientRequestIds.delete(clientId);
|
||||
}
|
||||
}
|
||||
|
||||
hasSubscribingClient(): boolean {
|
||||
return this.clientRequestIds.size > 0;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
Subscription
|
||||
}
|
||||
48
src/LiveQuery/equalObjects.js
Normal file
48
src/LiveQuery/equalObjects.js
Normal file
@@ -0,0 +1,48 @@
|
||||
var toString = Object.prototype.toString;
|
||||
|
||||
/**
|
||||
* Determines whether two objects represent the same primitive, special Parse
|
||||
* type, or full Parse Object.
|
||||
*/
|
||||
function equalObjects(a, b) {
|
||||
if (typeof a !== typeof b) {
|
||||
return false;
|
||||
}
|
||||
if (typeof a !== 'object') {
|
||||
return (a === b);
|
||||
}
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
if (toString.call(a) === '[object Date]') {
|
||||
if (toString.call(b) === '[object Date]') {
|
||||
return (+a === +b);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (Array.isArray(a)) {
|
||||
if (Array.isArray(b)) {
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
for (var i = 0; i < a.length; i++) {
|
||||
if (!equalObjects(a[i], b[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (Object.keys(a).length !== Object.keys(b).length) {
|
||||
return false;
|
||||
}
|
||||
for (var key in a) {
|
||||
if (!equalObjects(a[key], b[key])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
module.exports = equalObjects;
|
||||
@@ -266,6 +266,7 @@ RestWrite.prototype.findUsersWithAuthData = function(authData) {
|
||||
return findPromise;
|
||||
}
|
||||
|
||||
|
||||
RestWrite.prototype.handleAuthData = function(authData) {
|
||||
let results;
|
||||
return this.handleAuthDataValidation(authData).then(() => {
|
||||
@@ -768,7 +769,9 @@ RestWrite.prototype.runAfterTrigger = function() {
|
||||
}
|
||||
|
||||
// Avoid doing any setup for triggers if there is no 'afterSave' trigger for this class.
|
||||
if (!triggers.triggerExists(this.className, triggers.Types.afterSave, this.config.applicationId)) {
|
||||
let hasAfterSaveHook = triggers.triggerExists(this.className, triggers.Types.afterSave, this.config.applicationId);
|
||||
let hasLiveQuery = this.config.liveQueryController.hasLiveQuery(this.className);
|
||||
if (!hasAfterSaveHook && !hasLiveQuery) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@@ -789,6 +792,10 @@ RestWrite.prototype.runAfterTrigger = function() {
|
||||
updatedObject.set(this.sanitizedData());
|
||||
updatedObject._handleSaveResponse(this.response.response, this.response.status || 200);
|
||||
|
||||
// Notifiy LiveQueryServer if possible
|
||||
this.config.liveQueryController.onAfterSave(updatedObject.className, updatedObject, originalObject);
|
||||
|
||||
// Run afterSave trigger
|
||||
triggers.maybeRunTrigger(triggers.Types.afterSave, this.auth, updatedObject, originalObject, this.config.applicationId);
|
||||
};
|
||||
|
||||
|
||||
79
src/index.js
79
src/index.js
@@ -11,41 +11,43 @@ var batch = require('./batch'),
|
||||
Parse = require('parse/node').Parse,
|
||||
authDataManager = require('./authDataManager');
|
||||
|
||||
//import passwordReset from './passwordReset';
|
||||
import cache from './cache';
|
||||
import Config from './Config';
|
||||
import parseServerPackage from '../package.json';
|
||||
import ParsePushAdapter from './Adapters/Push/ParsePushAdapter';
|
||||
import PromiseRouter from './PromiseRouter';
|
||||
import requiredParameter from './requiredParameter';
|
||||
import { AnalyticsRouter } from './Routers/AnalyticsRouter';
|
||||
import { ClassesRouter } from './Routers/ClassesRouter';
|
||||
import { FeaturesRouter } from './Routers/FeaturesRouter';
|
||||
import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter';
|
||||
import { FilesController } from './Controllers/FilesController';
|
||||
import { FilesRouter } from './Routers/FilesRouter';
|
||||
import { FunctionsRouter } from './Routers/FunctionsRouter';
|
||||
import { GCSAdapter } from './Adapters/Files/GCSAdapter';
|
||||
import { GlobalConfigRouter } from './Routers/GlobalConfigRouter';
|
||||
import { GridStoreAdapter } from './Adapters/Files/GridStoreAdapter';
|
||||
import { HooksController } from './Controllers/HooksController';
|
||||
import { HooksRouter } from './Routers/HooksRouter';
|
||||
import { IAPValidationRouter } from './Routers/IAPValidationRouter';
|
||||
import { InstallationsRouter } from './Routers/InstallationsRouter';
|
||||
import { loadAdapter } from './Adapters/AdapterLoader';
|
||||
import { LoggerController } from './Controllers/LoggerController';
|
||||
import { LogsRouter } from './Routers/LogsRouter';
|
||||
import { PublicAPIRouter } from './Routers/PublicAPIRouter';
|
||||
import { PushController } from './Controllers/PushController';
|
||||
import { PushRouter } from './Routers/PushRouter';
|
||||
import { randomString } from './cryptoUtils';
|
||||
import { RolesRouter } from './Routers/RolesRouter';
|
||||
import { S3Adapter } from './Adapters/Files/S3Adapter';
|
||||
import { SchemasRouter } from './Routers/SchemasRouter';
|
||||
import { SessionsRouter } from './Routers/SessionsRouter';
|
||||
import { setFeature } from './features';
|
||||
import { UserController } from './Controllers/UserController';
|
||||
import { UsersRouter } from './Routers/UsersRouter';
|
||||
//import passwordReset from './passwordReset';
|
||||
import cache from './cache';
|
||||
import Config from './Config';
|
||||
import parseServerPackage from '../package.json';
|
||||
import ParsePushAdapter from './Adapters/Push/ParsePushAdapter';
|
||||
import PromiseRouter from './PromiseRouter';
|
||||
import requiredParameter from './requiredParameter';
|
||||
import { AnalyticsRouter } from './Routers/AnalyticsRouter';
|
||||
import { ClassesRouter } from './Routers/ClassesRouter';
|
||||
import { FeaturesRouter } from './Routers/FeaturesRouter';
|
||||
import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter';
|
||||
import { FilesController } from './Controllers/FilesController';
|
||||
import { FilesRouter } from './Routers/FilesRouter';
|
||||
import { FunctionsRouter } from './Routers/FunctionsRouter';
|
||||
import { GCSAdapter } from './Adapters/Files/GCSAdapter';
|
||||
import { GlobalConfigRouter } from './Routers/GlobalConfigRouter';
|
||||
import { GridStoreAdapter } from './Adapters/Files/GridStoreAdapter';
|
||||
import { HooksController } from './Controllers/HooksController';
|
||||
import { HooksRouter } from './Routers/HooksRouter';
|
||||
import { IAPValidationRouter } from './Routers/IAPValidationRouter';
|
||||
import { InstallationsRouter } from './Routers/InstallationsRouter';
|
||||
import { loadAdapter } from './Adapters/AdapterLoader';
|
||||
import { LiveQueryController } from './Controllers/LiveQueryController';
|
||||
import { LoggerController } from './Controllers/LoggerController';
|
||||
import { LogsRouter } from './Routers/LogsRouter';
|
||||
import { ParseLiveQueryServer } from './LiveQuery/ParseLiveQueryServer';
|
||||
import { PublicAPIRouter } from './Routers/PublicAPIRouter';
|
||||
import { PushController } from './Controllers/PushController';
|
||||
import { PushRouter } from './Routers/PushRouter';
|
||||
import { randomString } from './cryptoUtils';
|
||||
import { RolesRouter } from './Routers/RolesRouter';
|
||||
import { S3Adapter } from './Adapters/Files/S3Adapter';
|
||||
import { SchemasRouter } from './Routers/SchemasRouter';
|
||||
import { SessionsRouter } from './Routers/SessionsRouter';
|
||||
import { setFeature } from './features';
|
||||
import { UserController } from './Controllers/UserController';
|
||||
import { UsersRouter } from './Routers/UsersRouter';
|
||||
|
||||
// Mutate the Parse object to add the Cloud Code handlers
|
||||
addParseCloud();
|
||||
@@ -108,6 +110,7 @@ function ParseServer({
|
||||
choosePassword: undefined,
|
||||
passwordResetSuccess: undefined
|
||||
},
|
||||
liveQuery = {}
|
||||
}) {
|
||||
setFeature('serverVersion', parseServerPackage.version);
|
||||
// Initialize the node client SDK automatically
|
||||
@@ -151,6 +154,7 @@ function ParseServer({
|
||||
const loggerController = new LoggerController(loggerControllerAdapter, appId);
|
||||
const hooksController = new HooksController(appId, collectionPrefix);
|
||||
const userController = new UserController(emailControllerAdapter, appId, { verifyUserEmails });
|
||||
const liveQueryController = new LiveQueryController(liveQuery);
|
||||
|
||||
|
||||
cache.apps.set(appId, {
|
||||
@@ -174,6 +178,7 @@ function ParseServer({
|
||||
appName: appName,
|
||||
publicServerURL: publicServerURL,
|
||||
customPages: customPages,
|
||||
liveQueryController: liveQueryController
|
||||
});
|
||||
|
||||
// To maintain compatibility. TODO: Remove in some version that breaks backwards compatability
|
||||
@@ -262,6 +267,10 @@ function addParseCloud() {
|
||||
global.Parse = Parse;
|
||||
}
|
||||
|
||||
ParseServer.createLiveQueryServer = function(httpServer, config) {
|
||||
return new ParseLiveQueryServer(httpServer, config);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ParseServer: ParseServer,
|
||||
S3Adapter: S3Adapter,
|
||||
|
||||
@@ -42,6 +42,7 @@ function del(config, auth, className, objectId) {
|
||||
return Promise.resolve().then(() => {
|
||||
if (triggers.getTrigger(className, triggers.Types.beforeDelete, config.applicationId) ||
|
||||
triggers.getTrigger(className, triggers.Types.afterDelete, config.applicationId) ||
|
||||
(config.liveQueryController && config.liveQueryController.hasLiveQuery(className)) ||
|
||||
className == '_Session') {
|
||||
return find(config, Auth.master(config), className, {objectId: objectId})
|
||||
.then((response) => {
|
||||
@@ -49,6 +50,8 @@ function del(config, auth, className, objectId) {
|
||||
response.results[0].className = className;
|
||||
cache.users.remove(response.results[0].sessionToken);
|
||||
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);
|
||||
}
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
|
||||
@@ -97,7 +100,8 @@ function update(config, auth, className, objectId, restObject) {
|
||||
|
||||
return Promise.resolve().then(() => {
|
||||
if (triggers.getTrigger(className, triggers.Types.beforeSave, config.applicationId) ||
|
||||
triggers.getTrigger(className, triggers.Types.afterSave, config.applicationId)) {
|
||||
triggers.getTrigger(className, triggers.Types.afterSave, config.applicationId) ||
|
||||
(config.liveQueryController && config.liveQueryController.hasLiveQuery(className))) {
|
||||
return find(config, Auth.master(config), className, {objectId: objectId});
|
||||
}
|
||||
return Promise.resolve({});
|
||||
|
||||
@@ -3,6 +3,7 @@ import cache from './cache';
|
||||
import * as middlewares from './middlewares';
|
||||
import { ParseServer } from './index';
|
||||
import { Parse } from 'parse/node';
|
||||
|
||||
var express = require('express'),
|
||||
cryptoUtils = require('./cryptoUtils');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user