Support local time for scheduled pushes (#4137)
* Handle local push time * PR feedback * Improve timezone detection with regex * Use indexOf instead of endsWith * Add documentation * Add end to end test for scheduled pushes in local time * Revert changes to npm-git script * clean up
This commit is contained in:
@@ -138,8 +138,8 @@ describe('PushController', () => {
|
|||||||
'push_time': timeStr
|
'push_time': timeStr
|
||||||
}
|
}
|
||||||
|
|
||||||
var time = PushController.getPushTime(body);
|
var { date } = PushController.getPushTime(body);
|
||||||
expect(time).toEqual(new Date(timeStr));
|
expect(date).toEqual(new Date(timeStr));
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -150,8 +150,8 @@ describe('PushController', () => {
|
|||||||
'push_time': timeNumber
|
'push_time': timeNumber
|
||||||
}
|
}
|
||||||
|
|
||||||
var time = PushController.getPushTime(body).valueOf();
|
var { date } = PushController.getPushTime(body);
|
||||||
expect(time).toEqual(timeNumber * 1000);
|
expect(date.valueOf()).toEqual(timeNumber * 1000);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -640,16 +640,36 @@ describe('PushController', () => {
|
|||||||
expect(PushController.getPushTime()).toBe(undefined);
|
expect(PushController.getPushTime()).toBe(undefined);
|
||||||
expect(PushController.getPushTime({
|
expect(PushController.getPushTime({
|
||||||
'push_time': 1000
|
'push_time': 1000
|
||||||
})).toEqual(new Date(1000 * 1000));
|
}).date).toEqual(new Date(1000 * 1000));
|
||||||
expect(PushController.getPushTime({
|
expect(PushController.getPushTime({
|
||||||
'push_time': '2017-01-01'
|
'push_time': '2017-01-01'
|
||||||
})).toEqual(new Date('2017-01-01'));
|
}).date).toEqual(new Date('2017-01-01'));
|
||||||
|
|
||||||
expect(() => {PushController.getPushTime({
|
expect(() => {PushController.getPushTime({
|
||||||
'push_time': 'gibberish-time'
|
'push_time': 'gibberish-time'
|
||||||
})}).toThrow();
|
})}).toThrow();
|
||||||
expect(() => {PushController.getPushTime({
|
expect(() => {PushController.getPushTime({
|
||||||
'push_time': Number.NaN
|
'push_time': Number.NaN
|
||||||
})}).toThrow();
|
})}).toThrow();
|
||||||
|
|
||||||
|
expect(PushController.getPushTime({
|
||||||
|
push_time: '2017-09-06T13:42:48.369Z'
|
||||||
|
})).toEqual({
|
||||||
|
date: new Date('2017-09-06T13:42:48.369Z'),
|
||||||
|
isLocalTime: false,
|
||||||
|
});
|
||||||
|
expect(PushController.getPushTime({
|
||||||
|
push_time: '2007-04-05T12:30-02:00',
|
||||||
|
})).toEqual({
|
||||||
|
date: new Date('2007-04-05T12:30-02:00'),
|
||||||
|
isLocalTime: false,
|
||||||
|
});
|
||||||
|
expect(PushController.getPushTime({
|
||||||
|
push_time: '2007-04-05T12:30',
|
||||||
|
})).toEqual({
|
||||||
|
date: new Date('2007-04-05T12:30'),
|
||||||
|
isLocalTime: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not schedule push when not configured', (done) => {
|
it('should not schedule push when not configured', (done) => {
|
||||||
@@ -979,4 +999,86 @@ describe('PushController', () => {
|
|||||||
done();
|
done();
|
||||||
}).catch(done.fail);
|
}).catch(done.fail);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('pushTimeHasTimezoneComponent', () => {
|
||||||
|
it('should be accurate', () => {
|
||||||
|
expect(PushController.pushTimeHasTimezoneComponent('2017-09-06T17:14:01.048Z'))
|
||||||
|
.toBe(true, 'UTC time');
|
||||||
|
expect(PushController.pushTimeHasTimezoneComponent('2007-04-05T12:30-02:00'))
|
||||||
|
.toBe(true, 'Timezone offset');
|
||||||
|
expect(PushController.pushTimeHasTimezoneComponent('2007-04-05T12:30:00.000Z-02:00'))
|
||||||
|
.toBe(true, 'Seconds + Milliseconds + Timezone offset');
|
||||||
|
|
||||||
|
expect(PushController.pushTimeHasTimezoneComponent('2017-09-06T17:14:01.048'))
|
||||||
|
.toBe(false, 'No timezone');
|
||||||
|
expect(PushController.pushTimeHasTimezoneComponent('2017-09-06'))
|
||||||
|
.toBe(false, 'YY-MM-DD');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatPushTime', () => {
|
||||||
|
it('should format as ISO string', () => {
|
||||||
|
expect(PushController.formatPushTime({
|
||||||
|
date: new Date('2017-09-06T17:14:01.048Z'),
|
||||||
|
isLocalTime: false,
|
||||||
|
})).toBe('2017-09-06T17:14:01.048Z', 'UTC time');
|
||||||
|
expect(PushController.formatPushTime({
|
||||||
|
date: new Date('2007-04-05T12:30-02:00'),
|
||||||
|
isLocalTime: false
|
||||||
|
})).toBe('2007-04-05T14:30:00.000Z', 'Timezone offset');
|
||||||
|
|
||||||
|
expect(PushController.formatPushTime({
|
||||||
|
date: new Date('2017-09-06T17:14:01.048'),
|
||||||
|
isLocalTime: true,
|
||||||
|
})).toBe('2017-09-06T17:14:01.048', 'No timezone');
|
||||||
|
expect(PushController.formatPushTime({
|
||||||
|
date: new Date('2017-09-06'),
|
||||||
|
isLocalTime: true
|
||||||
|
})).toBe('2017-09-06T00:00:00.000', 'YY-MM-DD');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Scheduling pushes in local time', () => {
|
||||||
|
it('should preserve the push time', (done) => {
|
||||||
|
const auth = {isMaster: true};
|
||||||
|
const pushAdapter = {
|
||||||
|
send(body, installations) {
|
||||||
|
return successfulTransmissions(body, installations);
|
||||||
|
},
|
||||||
|
getValidPushTypes() {
|
||||||
|
return ["ios"];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const pushTime = '2017-09-06T17:14:01.048';
|
||||||
|
|
||||||
|
reconfigureServer({
|
||||||
|
push: {adapter: pushAdapter},
|
||||||
|
scheduledPush: true
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
const config = new Config(Parse.applicationId);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const pushController = new PushController();
|
||||||
|
pushController.sendPush({
|
||||||
|
data: {
|
||||||
|
alert: "Hello World!",
|
||||||
|
badge: "Increment",
|
||||||
|
},
|
||||||
|
push_time: pushTime
|
||||||
|
}, {}, config, auth, resolve)
|
||||||
|
.catch(reject);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then((pushStatusId) => {
|
||||||
|
const q = new Parse.Query('_PushStatus');
|
||||||
|
return q.get(pushStatusId, {useMasterKey: true});
|
||||||
|
})
|
||||||
|
.then((pushStatus) => {
|
||||||
|
expect(pushStatus.get('status')).toBe('scheduled');
|
||||||
|
expect(pushStatus.get('pushTime')).toBe('2017-09-06T17:14:01.048');
|
||||||
|
})
|
||||||
|
.then(done, done.fail);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,10 +14,11 @@ export class PushController {
|
|||||||
}
|
}
|
||||||
// Replace the expiration_time and push_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);
|
||||||
const push_time = PushController.getPushTime(body);
|
const pushTime = PushController.getPushTime(body);
|
||||||
if (typeof push_time !== 'undefined') {
|
if (pushTime && pushTime.date !== 'undefined') {
|
||||||
body['push_time'] = push_time;
|
body['push_time'] = PushController.formatPushTime(pushTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 = () => {
|
||||||
@@ -104,21 +105,53 @@ export class PushController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var pushTimeParam = body['push_time'];
|
var pushTimeParam = body['push_time'];
|
||||||
var pushTime;
|
var date;
|
||||||
|
var isLocalTime = true;
|
||||||
|
|
||||||
if (typeof pushTimeParam === 'number') {
|
if (typeof pushTimeParam === 'number') {
|
||||||
pushTime = new Date(pushTimeParam * 1000);
|
date = new Date(pushTimeParam * 1000);
|
||||||
} else if (typeof pushTimeParam === 'string') {
|
} else if (typeof pushTimeParam === 'string') {
|
||||||
pushTime = new Date(pushTimeParam);
|
isLocalTime = !PushController.pushTimeHasTimezoneComponent(pushTimeParam);
|
||||||
|
date = new Date(pushTimeParam);
|
||||||
} else {
|
} else {
|
||||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||||
body['push_time'] + ' is not valid time.');
|
body['push_time'] + ' is not valid time.');
|
||||||
}
|
}
|
||||||
// Check pushTime is valid or not, if it is not valid, pushTime is NaN
|
// Check pushTime is valid or not, if it is not valid, pushTime is NaN
|
||||||
if (!isFinite(pushTime)) {
|
if (!isFinite(date)) {
|
||||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||||
body['push_time'] + ' is not valid time.');
|
body['push_time'] + ' is not valid time.');
|
||||||
}
|
}
|
||||||
return pushTime;
|
|
||||||
|
return {
|
||||||
|
date,
|
||||||
|
isLocalTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a ISO8601 formatted date contains a timezone component
|
||||||
|
* @param pushTimeParam {string}
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
static pushTimeHasTimezoneComponent(pushTimeParam: string): boolean {
|
||||||
|
const offsetPattern = /(.+)([+-])\d\d:\d\d$/;
|
||||||
|
return pushTimeParam.indexOf('Z') === pushTimeParam.length - 1 // 2007-04-05T12:30Z
|
||||||
|
|| offsetPattern.test(pushTimeParam); // 2007-04-05T12:30.000+02:00, 2007-04-05T12:30.000-02:00
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a date to ISO format in UTC time and strips the timezone if `isLocalTime` is true
|
||||||
|
* @param date {Date}
|
||||||
|
* @param isLocalTime {boolean}
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
static formatPushTime({ date, isLocalTime }: { date: Date, isLocalTime: boolean }) {
|
||||||
|
if (isLocalTime) { // Strip 'Z'
|
||||||
|
const isoString = date.toISOString();
|
||||||
|
return isoString.substring(0, isoString.indexOf('Z'));
|
||||||
|
}
|
||||||
|
return date.toISOString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ export function pushStatusHandler(config, objectId = newObjectId(config.objectId
|
|||||||
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 pushTime = now.toISOString();
|
||||||
let status = 'pending';
|
let status = 'pending';
|
||||||
if (body.hasOwnProperty('push_time')) {
|
if (body.hasOwnProperty('push_time')) {
|
||||||
if (config.hasPushScheduledSupport) {
|
if (config.hasPushScheduledSupport) {
|
||||||
@@ -135,7 +135,7 @@ export function pushStatusHandler(config, objectId = newObjectId(config.objectId
|
|||||||
const object = {
|
const object = {
|
||||||
objectId,
|
objectId,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
pushTime: pushTime.toISOString(),
|
pushTime,
|
||||||
query: JSON.stringify(where),
|
query: JSON.stringify(where),
|
||||||
payload: payloadString,
|
payload: payloadString,
|
||||||
source: options.source,
|
source: options.source,
|
||||||
|
|||||||
Reference in New Issue
Block a user