Skip to content

Adds support for PushScheduling #3722

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Apr 15, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 126 additions & 1 deletion spec/PushController.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});
1 change: 1 addition & 0 deletions src/Config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
38 changes: 35 additions & 3 deletions src/Controllers/PushController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down Expand Up @@ -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(() => {
Expand All @@ -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;
}
Expand All @@ -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;
5 changes: 4 additions & 1 deletion src/ParseServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ class ParseServer {
analyticsAdapter,
filesAdapter,
push,
scheduledPush = false,
loggerAdapter,
jsonLogs = defaults.jsonLogs,
logsFolder = defaults.logsFolder,
Expand Down Expand Up @@ -182,6 +183,7 @@ class ParseServer {
const pushController = new PushController();

const hasPushSupport = pushAdapter && push;
const hasPushScheduledSupport = pushAdapter && push && scheduledPush;

const {
disablePushWorker
Expand Down Expand Up @@ -259,7 +261,8 @@ class ParseServer {
userSensitiveFields,
pushWorker,
pushControllerQueue,
hasPushSupport
hasPushSupport,
hasPushScheduledSupport
});

Config.validate(AppCache.get(appId));
Expand Down
2 changes: 1 addition & 1 deletion src/Routers/FeaturesRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
16 changes: 14 additions & 2 deletions src/StatusHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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!
Expand Down
5 changes: 5 additions & 0 deletions src/cli/definitions/parse-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down