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', () => {
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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!
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user