Adds support for PushScheduling (#3722)

* Add support for push scheduling

Add a configuration flag on the server to handle the availability of
push scheduling.

* Update push controller to skip sending only if scheduling is configured

Only skip push sending if scheduling is configured

* Update bad conventions

* Add CLI definitions for push scheduling

* Adds tests for pushTime

* Adds test for scheduling

* nits

* Test for not scheduled
This commit is contained in:
Florent Vilmart
2017-04-15 17:20:55 -04:00
committed by GitHub
parent 302a0dda73
commit 907b160fc7
7 changed files with 186 additions and 8 deletions

View File

@@ -531,5 +531,130 @@ describe('PushController', () => {
it('should flatten', () => { it('should flatten', () => {
var res = StatusHandler.flatten([1, [2], [[3, 4], 5], [[[6]]]]) var res = StatusHandler.flatten([1, [2], [[3, 4], 5], [[[6]]]])
expect(res).toEqual([1,2,3,4,5,6]); expect(res).toEqual([1,2,3,4,5,6]);
}) });
it('properly transforms push time', () => {
expect(PushController.getPushTime()).toBe(undefined);
expect(PushController.getPushTime({
'push_time': 1000
})).toEqual(new Date(1000 * 1000));
expect(PushController.getPushTime({
'push_time': '2017-01-01'
})).toEqual(new Date('2017-01-01'));
expect(() => {PushController.getPushTime({
'push_time': 'gibberish-time'
})}).toThrow();
expect(() => {PushController.getPushTime({
'push_time': Number.NaN
})}).toThrow();
});
it('should not schedule push when not configured', (done) => {
var config = new Config(Parse.applicationId);
var auth = {
isMaster: true
}
var pushAdapter = {
send: function(body, installations) {
return successfulTransmissions(body, installations);
},
getValidPushTypes: function() {
return ["ios"];
}
}
var pushController = new PushController();
const payload = {
data: {
alert: 'hello',
},
push_time: new Date().getTime()
}
var installations = [];
while(installations.length != 10) {
const installation = new Parse.Object("_Installation");
installation.set("installationId", "installation_" + installations.length);
installation.set("deviceToken","device_token_" + installations.length)
installation.set("badge", installations.length);
installation.set("originalBadge", installations.length);
installation.set("deviceType", "ios");
installations.push(installation);
}
reconfigureServer({
push: { adapter: pushAdapter }
}).then(() => {
return Parse.Object.saveAll(installations).then(() => {
return pushController.sendPush(payload, {}, config, auth);
});
}).then(() => {
const query = new Parse.Query('_PushStatus');
return query.find({useMasterKey: true}).then((results) => {
expect(results.length).toBe(1);
const pushStatus = results[0];
expect(pushStatus.get('status')).not.toBe('scheduled');
done();
});
}).catch((err) => {
console.error(err);
fail('should not fail');
done();
});
});
it('should not schedule push when configured', (done) => {
var auth = {
isMaster: true
}
var pushAdapter = {
send: function(body, installations) {
return successfulTransmissions(body, installations);
},
getValidPushTypes: function() {
return ["ios"];
}
}
var pushController = new PushController();
const payload = {
data: {
alert: 'hello',
},
push_time: new Date().getTime() / 1000
}
var installations = [];
while(installations.length != 10) {
const installation = new Parse.Object("_Installation");
installation.set("installationId", "installation_" + installations.length);
installation.set("deviceToken","device_token_" + installations.length)
installation.set("badge", installations.length);
installation.set("originalBadge", installations.length);
installation.set("deviceType", "ios");
installations.push(installation);
}
reconfigureServer({
push: { adapter: pushAdapter },
scheduledPush: true
}).then(() => {
var config = new Config(Parse.applicationId);
return Parse.Object.saveAll(installations).then(() => {
return pushController.sendPush(payload, {}, config, auth);
});
}).then(() => {
const query = new Parse.Query('_PushStatus');
return query.find({useMasterKey: true}).then((results) => {
expect(results.length).toBe(1);
const pushStatus = results[0];
expect(pushStatus.get('status')).toBe('scheduled');
done();
});
}).catch((err) => {
console.error(err);
fail('should not fail');
done();
});
});
}); });

View File

@@ -61,6 +61,7 @@ export class Config {
this.pushControllerQueue = cacheInfo.pushControllerQueue; this.pushControllerQueue = cacheInfo.pushControllerQueue;
this.pushWorker = cacheInfo.pushWorker; this.pushWorker = cacheInfo.pushWorker;
this.hasPushSupport = cacheInfo.hasPushSupport; this.hasPushSupport = cacheInfo.hasPushSupport;
this.hasPushScheduledSupport = cacheInfo.hasPushScheduledSupport;
this.loggerController = cacheInfo.loggerController; this.loggerController = cacheInfo.loggerController;
this.userController = cacheInfo.userController; this.userController = cacheInfo.userController;
this.authDataManager = cacheInfo.authDataManager; this.authDataManager = cacheInfo.authDataManager;

View File

@@ -12,8 +12,9 @@ export class PushController {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'Missing push configuration'); 'Missing push configuration');
} }
// Replace the expiration_time with a valid Unix epoch milliseconds time // Replace the expiration_time and push_time with a valid Unix epoch milliseconds time
body['expiration_time'] = PushController.getExpirationTime(body); body.expiration_time = PushController.getExpirationTime(body);
body.push_time = PushController.getPushTime(body);
// TODO: If the req can pass the checking, we return immediately instead of waiting // TODO: If the req can pass the checking, we return immediately instead of waiting
// pushes to be sent. We probably change this behaviour in the future. // pushes to be sent. We probably change this behaviour in the future.
let badgeUpdate = () => { let badgeUpdate = () => {
@@ -49,6 +50,9 @@ export class PushController {
onPushStatusSaved(pushStatus.objectId); onPushStatusSaved(pushStatus.objectId);
return badgeUpdate(); return badgeUpdate();
}).then(() => { }).then(() => {
if (body.push_time && config.hasPushScheduledSupport) {
return Promise.resolve();
}
return config.pushControllerQueue.enqueue(body, where, config, auth, pushStatus); return config.pushControllerQueue.enqueue(body, where, config, auth, pushStatus);
}).catch((err) => { }).catch((err) => {
return pushStatus.fail(err).then(() => { return pushStatus.fail(err).then(() => {
@@ -63,7 +67,7 @@ export class PushController {
* @returns {Number|undefined} The expiration time if it exists in the request * @returns {Number|undefined} The expiration time if it exists in the request
*/ */
static getExpirationTime(body = {}) { static getExpirationTime(body = {}) {
var hasExpirationTime = !!body['expiration_time']; var hasExpirationTime = body.hasOwnProperty('expiration_time');
if (!hasExpirationTime) { if (!hasExpirationTime) {
return; return;
} }
@@ -84,6 +88,34 @@ export class PushController {
} }
return expirationTime.valueOf(); return expirationTime.valueOf();
} }
/**
* Get push time from the request body.
* @param {Object} request A request object
* @returns {Number|undefined} The push time if it exists in the request
*/
static getPushTime(body = {}) {
var hasPushTime = body.hasOwnProperty('push_time');
if (!hasPushTime) {
return;
}
var pushTimeParam = body['push_time'];
var pushTime;
if (typeof pushTimeParam === 'number') {
pushTime = new Date(pushTimeParam * 1000);
} else if (typeof pushTimeParam === 'string') {
pushTime = new Date(pushTimeParam);
} else {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
body['push_time'] + ' is not valid time.');
}
// Check pushTime is valid or not, if it is not valid, pushTime is NaN
if (!isFinite(pushTime)) {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
body['push_time'] + ' is not valid time.');
}
return pushTime;
}
} }
export default PushController; export default PushController;

View File

@@ -95,6 +95,7 @@ class ParseServer {
analyticsAdapter, analyticsAdapter,
filesAdapter, filesAdapter,
push, push,
scheduledPush = false,
loggerAdapter, loggerAdapter,
jsonLogs = defaults.jsonLogs, jsonLogs = defaults.jsonLogs,
logsFolder = defaults.logsFolder, logsFolder = defaults.logsFolder,
@@ -182,6 +183,7 @@ class ParseServer {
const pushController = new PushController(); const pushController = new PushController();
const hasPushSupport = pushAdapter && push; const hasPushSupport = pushAdapter && push;
const hasPushScheduledSupport = pushAdapter && push && scheduledPush;
const { const {
disablePushWorker disablePushWorker
@@ -259,7 +261,8 @@ class ParseServer {
userSensitiveFields, userSensitiveFields,
pushWorker, pushWorker,
pushControllerQueue, pushControllerQueue,
hasPushSupport hasPushSupport,
hasPushScheduledSupport
}); });
Config.validate(AppCache.get(appId)); Config.validate(AppCache.get(appId));

View File

@@ -30,7 +30,7 @@ export class FeaturesRouter extends PromiseRouter {
}, },
push: { push: {
immediatePush: req.config.hasPushSupport, immediatePush: req.config.hasPushSupport,
scheduledPush: false, scheduledPush: req.config.hasPushScheduledSupport,
storedPushData: req.config.hasPushSupport, storedPushData: req.config.hasPushSupport,
pushAudiences: false, pushAudiences: false,
}, },

View File

@@ -110,6 +110,18 @@ export function pushStatusHandler(config, objectId = newObjectId()) {
const handler = statusHandler(PUSH_STATUS_COLLECTION, database); const handler = statusHandler(PUSH_STATUS_COLLECTION, database);
const setInitial = function(body = {}, where, options = {source: 'rest'}) { const setInitial = function(body = {}, where, options = {source: 'rest'}) {
const now = new Date(); const now = new Date();
let pushTime = new Date();
let status = 'pending';
if (body.hasOwnProperty('push_time')) {
if (config.hasPushScheduledSupport) {
pushTime = body.push_time;
status = 'scheduled';
} else {
logger.warn('Trying to schedule a push while server is not configured.');
logger.warn('Push will be sent immediately');
}
}
const data = body.data || {}; const data = body.data || {};
const payloadString = JSON.stringify(data); const payloadString = JSON.stringify(data);
let pushHash; let pushHash;
@@ -123,13 +135,13 @@ export function pushStatusHandler(config, objectId = newObjectId()) {
const object = { const object = {
objectId, objectId,
createdAt: now, createdAt: now,
pushTime: now.toISOString(), pushTime: pushTime.toISOString(),
query: JSON.stringify(where), query: JSON.stringify(where),
payload: payloadString, payload: payloadString,
source: options.source, source: options.source,
title: options.title, title: options.title,
expiry: body.expiration_time, expiry: body.expiration_time,
status: "pending", status: status,
numSent: 0, numSent: 0,
pushHash, pushHash,
// lockdown! // lockdown!

View File

@@ -81,6 +81,11 @@ export default {
help: "Configuration for push, as stringified JSON. See https://github.com/ParsePlatform/parse-server/wiki/Push", help: "Configuration for push, as stringified JSON. See https://github.com/ParsePlatform/parse-server/wiki/Push",
action: objectParser action: objectParser
}, },
"scheduledPush": {
env: "PARSE_SERVER_SCHEDULED_PUSH",
help: "Configuration for push scheduling. Defaults to false.",
action: booleanParser
},
"oauth": { "oauth": {
env: "PARSE_SERVER_OAUTH_PROVIDERS", env: "PARSE_SERVER_OAUTH_PROVIDERS",
help: "[DEPRECATED (use auth option)] Configuration for your oAuth providers, as stringified JSON. See https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#oauth", help: "[DEPRECATED (use auth option)] Configuration for your oAuth providers, as stringified JSON. See https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#oauth",