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:
@@ -531,5 +531,130 @@ describe('PushController', () => {
|
||||
it('should flatten', () => {
|
||||
var res = StatusHandler.flatten([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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -61,6 +61,7 @@ export class Config {
|
||||
this.pushControllerQueue = cacheInfo.pushControllerQueue;
|
||||
this.pushWorker = cacheInfo.pushWorker;
|
||||
this.hasPushSupport = cacheInfo.hasPushSupport;
|
||||
this.hasPushScheduledSupport = cacheInfo.hasPushScheduledSupport;
|
||||
this.loggerController = cacheInfo.loggerController;
|
||||
this.userController = cacheInfo.userController;
|
||||
this.authDataManager = cacheInfo.authDataManager;
|
||||
|
||||
@@ -12,8 +12,9 @@ export class PushController {
|
||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||
'Missing push configuration');
|
||||
}
|
||||
// Replace the expiration_time with a valid Unix epoch milliseconds time
|
||||
body['expiration_time'] = PushController.getExpirationTime(body);
|
||||
// Replace the expiration_time and push_time with a valid Unix epoch milliseconds time
|
||||
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
|
||||
// pushes to be sent. We probably change this behaviour in the future.
|
||||
let badgeUpdate = () => {
|
||||
@@ -49,6 +50,9 @@ export class PushController {
|
||||
onPushStatusSaved(pushStatus.objectId);
|
||||
return badgeUpdate();
|
||||
}).then(() => {
|
||||
if (body.push_time && config.hasPushScheduledSupport) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return config.pushControllerQueue.enqueue(body, where, config, auth, pushStatus);
|
||||
}).catch((err) => {
|
||||
return pushStatus.fail(err).then(() => {
|
||||
@@ -63,7 +67,7 @@ export class PushController {
|
||||
* @returns {Number|undefined} The expiration time if it exists in the request
|
||||
*/
|
||||
static getExpirationTime(body = {}) {
|
||||
var hasExpirationTime = !!body['expiration_time'];
|
||||
var hasExpirationTime = body.hasOwnProperty('expiration_time');
|
||||
if (!hasExpirationTime) {
|
||||
return;
|
||||
}
|
||||
@@ -84,6 +88,34 @@ export class PushController {
|
||||
}
|
||||
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;
|
||||
|
||||
@@ -95,6 +95,7 @@ class ParseServer {
|
||||
analyticsAdapter,
|
||||
filesAdapter,
|
||||
push,
|
||||
scheduledPush = false,
|
||||
loggerAdapter,
|
||||
jsonLogs = defaults.jsonLogs,
|
||||
logsFolder = defaults.logsFolder,
|
||||
@@ -182,6 +183,7 @@ class ParseServer {
|
||||
const pushController = new PushController();
|
||||
|
||||
const hasPushSupport = pushAdapter && push;
|
||||
const hasPushScheduledSupport = pushAdapter && push && scheduledPush;
|
||||
|
||||
const {
|
||||
disablePushWorker
|
||||
@@ -259,7 +261,8 @@ class ParseServer {
|
||||
userSensitiveFields,
|
||||
pushWorker,
|
||||
pushControllerQueue,
|
||||
hasPushSupport
|
||||
hasPushSupport,
|
||||
hasPushScheduledSupport
|
||||
});
|
||||
|
||||
Config.validate(AppCache.get(appId));
|
||||
|
||||
@@ -30,7 +30,7 @@ export class FeaturesRouter extends PromiseRouter {
|
||||
},
|
||||
push: {
|
||||
immediatePush: req.config.hasPushSupport,
|
||||
scheduledPush: false,
|
||||
scheduledPush: req.config.hasPushScheduledSupport,
|
||||
storedPushData: req.config.hasPushSupport,
|
||||
pushAudiences: false,
|
||||
},
|
||||
|
||||
@@ -110,6 +110,18 @@ export function pushStatusHandler(config, objectId = newObjectId()) {
|
||||
const handler = statusHandler(PUSH_STATUS_COLLECTION, database);
|
||||
const setInitial = function(body = {}, where, options = {source: 'rest'}) {
|
||||
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 payloadString = JSON.stringify(data);
|
||||
let pushHash;
|
||||
@@ -123,13 +135,13 @@ export function pushStatusHandler(config, objectId = newObjectId()) {
|
||||
const object = {
|
||||
objectId,
|
||||
createdAt: now,
|
||||
pushTime: now.toISOString(),
|
||||
pushTime: pushTime.toISOString(),
|
||||
query: JSON.stringify(where),
|
||||
payload: payloadString,
|
||||
source: options.source,
|
||||
title: options.title,
|
||||
expiry: body.expiration_time,
|
||||
status: "pending",
|
||||
status: status,
|
||||
numSent: 0,
|
||||
pushHash,
|
||||
// lockdown!
|
||||
|
||||
@@ -81,6 +81,11 @@ export default {
|
||||
help: "Configuration for push, as stringified JSON. See https://github.com/ParsePlatform/parse-server/wiki/Push",
|
||||
action: objectParser
|
||||
},
|
||||
"scheduledPush": {
|
||||
env: "PARSE_SERVER_SCHEDULED_PUSH",
|
||||
help: "Configuration for push scheduling. Defaults to false.",
|
||||
action: booleanParser
|
||||
},
|
||||
"oauth": {
|
||||
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",
|
||||
|
||||
Reference in New Issue
Block a user