Skip to content
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

Refactor pushStatusHandler to use Parse instead of direct access #4173

Merged
merged 8 commits into from
Sep 18, 2017
15 changes: 15 additions & 0 deletions spec/CloudCode.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1627,4 +1627,19 @@ describe('afterFind hooks', () => {
})
.then(() => done());
});

it('should validate triggers correctly', () => {
expect(() => {
Parse.Cloud.beforeSave('_Session', () => {});
}).toThrow('Triggers are not supported for _Session class.');
expect(() => {
Parse.Cloud.afterSave('_Session', () => {});
}).toThrow('Triggers are not supported for _Session class.');
expect(() => {
Parse.Cloud.beforeSave('_PushStatus', () => {});
}).toThrow('Only afterSave is allowed on _PushStatus');
expect(() => {
Parse.Cloud.afterSave('_PushStatus', () => {});
}).not.toThrow();
});
});
2 changes: 2 additions & 0 deletions spec/Parse.Push.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ describe('Parse.Push', () => {
alert: 'Hello world!'
}
}, {useMasterKey: true}))
.then(() => delayPromise(500))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can these be any shorter?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nope, not really, because we don't wait till it's completed...

.then(() => {
request.get({
url: 'http://localhost:8378/1/classes/_PushStatus',
Expand Down Expand Up @@ -155,6 +156,7 @@ describe('Parse.Push', () => {
alert: 'Hello world!'
}
}, {useMasterKey: true}))
.then(() => delayPromise(500)) // put a delay as we keep writing
.then(() => {
request.get({
url: 'http://localhost:8378/1/classes/_PushStatus',
Expand Down
48 changes: 38 additions & 10 deletions spec/PushController.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,11 @@ describe('PushController', () => {
});

it('properly creates _PushStatus', (done) => {
const pushStatusAfterSave = {
handler: function() {}
};
const spy = spyOn(pushStatusAfterSave, 'handler').and.callThrough();
Parse.Cloud.afterSave('_PushStatus', pushStatusAfterSave.handler);
var installations = [];
while(installations.length != 10) {
const installation = new Parse.Object("_Installation");
Expand Down Expand Up @@ -466,8 +471,36 @@ describe('PushController', () => {
return query.find();
}).catch((error) => {
expect(error.code).toBe(119);
done();
});
})
.then(() => {
function getPushStatus(callIndex) {
return spy.calls.all()[callIndex].args[0].object;
}
expect(spy).toHaveBeenCalled();
expect(spy.calls.count()).toBe(4);
const allCalls = spy.calls.all();
allCalls.forEach((call) => {
expect(call.args.length).toBe(2);
const object = call.args[0].object;
expect(object instanceof Parse.Object).toBe(true);
});
expect(getPushStatus(0).get('status')).toBe('pending');
expect(getPushStatus(1).get('status')).toBe('running');
expect(getPushStatus(1).get('numSent')).toBe(0);
expect(getPushStatus(2).get('status')).toBe('running');
expect(getPushStatus(2).get('numSent')).toBe(10);
expect(getPushStatus(2).get('numFailed')).toBe(5);
// Those are updated from a nested . operation, this would
// not render correctly before
expect(getPushStatus(2).get('failedPerType')).toEqual({
android: 5
});
expect(getPushStatus(2).get('sentPerType')).toEqual({
ios: 10
});
expect(getPushStatus(3).get('status')).toBe('succeeded');
})
.then(done).catch(done.fail);
});

it('should properly report failures in _PushStatus', (done) => {
Expand Down Expand Up @@ -710,7 +743,7 @@ describe('PushController', () => {
}).then(() => {
return Parse.Object.saveAll(installations).then(() => {
return pushController.sendPush(payload, {}, config, auth);
});
}).then(() => new Promise(resolve => setTimeout(resolve, 300)));
}).then(() => {
const query = new Parse.Query('_PushStatus');
return query.find({useMasterKey: true}).then((results) => {
Expand Down Expand Up @@ -776,20 +809,15 @@ describe('PushController', () => {
var config = new Config(Parse.applicationId);
return Parse.Object.saveAll(installations).then(() => {
return pushController.sendPush(payload, {}, config, auth);
}).then(() => new Promise(resolve => setTimeout(resolve, 100)));
}).then(() => new Promise(resolve => setTimeout(resolve, 300)));
}).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();
});
}).then(done).catch(done.err);
});

it('should not enqueue push when device token is not set', (done) => {
Expand Down
31 changes: 10 additions & 21 deletions spec/PushWorker.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ describe('PushWorker', () => {
const spy = spyOn(config.database, "update").and.callFake(() => {
return Promise.resolve();
});
handler.trackSent([
const toAwait = handler.trackSent([
{
transmitted: false,
device: {
Expand Down Expand Up @@ -239,13 +239,13 @@ describe('PushWorker', () => {
expect(lastCall.args[2]).toEqual({
deviceToken: { '__op': "Delete" }
});
done();
toAwait.then(done).catch(done);
});

it('tracks push status per UTC offsets', (done) => {
const config = new Config('test');
const handler = pushStatusHandler(config, 'ABCDEF1234');
const spy = spyOn(config.database, "update").and.callThrough();
const handler = pushStatusHandler(config);
const spy = spyOn(Parse, "_request").and.callThrough();
const UTCOffset = 1;
handler.setInitial().then(() => {
return handler.trackSent([
Expand All @@ -266,14 +266,9 @@ describe('PushWorker', () => {
], UTCOffset)
}).then(() => {
expect(spy).toHaveBeenCalled();
expect(spy.calls.count()).toBe(1);
const lastCall = spy.calls.mostRecent();
expect(lastCall.args[0]).toBe('_PushStatus');
const updatePayload = lastCall.args[2];
expect(updatePayload.updatedAt instanceof Date).toBeTruthy();
// remove the updatedAt as not testable
delete updatePayload.updatedAt;

expect(lastCall.args[0]).toBe('PUT');
expect(lastCall.args[1]).toBe(`classes/_PushStatus/${handler.objectId}`);
expect(lastCall.args[2]).toEqual({
numSent: { __op: 'Increment', amount: 1 },
numFailed: { __op: 'Increment', amount: 1 },
Expand All @@ -284,7 +279,7 @@ describe('PushWorker', () => {
count: { __op: 'Increment', amount: -2 },
});
const query = new Parse.Query('_PushStatus');
return query.get('ABCDEF1234', { useMasterKey: true });
return query.get(handler.objectId, { useMasterKey: true });
}).then((pushStatus) => {
const sentPerUTCOffset = pushStatus.get('sentPerUTCOffset');
expect(sentPerUTCOffset['1']).toBe(1);
Expand Down Expand Up @@ -315,7 +310,7 @@ describe('PushWorker', () => {
], UTCOffset)
}).then(() => {
const query = new Parse.Query('_PushStatus');
return query.get('ABCDEF1234', { useMasterKey: true });
return query.get(handler.objectId, { useMasterKey: true });
}).then((pushStatus) => {
const sentPerUTCOffset = pushStatus.get('sentPerUTCOffset');
expect(sentPerUTCOffset['1']).toBe(3);
Expand All @@ -330,7 +325,7 @@ describe('PushWorker', () => {
spyOn(config.database, "create").and.callFake(() => {
return Promise.resolve();
});
const spy = spyOn(config.database, "update").and.callFake(() => {
const spy = spyOn(Parse, "_request").and.callFake(() => {
return Promise.resolve();
});
const UTCOffset = -6;
Expand All @@ -353,14 +348,8 @@ describe('PushWorker', () => {
},
], UTCOffset).then(() => {
expect(spy).toHaveBeenCalled();
expect(spy.calls.count()).toBe(1);
const lastCall = spy.calls.mostRecent();
expect(lastCall.args[0]).toBe('_PushStatus');
const updatePayload = lastCall.args[2];
expect(updatePayload.updatedAt instanceof Date).toBeTruthy();
// remove the updatedAt as not testable
delete updatePayload.updatedAt;

expect(lastCall.args[1]).toBe(`classes/_PushStatus/${handler.objectId}`);
expect(lastCall.args[2]).toEqual({
numSent: { __op: 'Increment', amount: 1 },
numFailed: { __op: 'Increment', amount: 1 },
Expand Down
16 changes: 15 additions & 1 deletion src/Controllers/DatabaseController.js
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,19 @@ DatabaseController.prototype.update = function(className, query, update, {
});
};

function expandResultOnKeyPath(object, key, value) {
if (key.indexOf('.') < 0) {
object[key] = value[key];
return object;
}
const path = key.split('.');
const firstKey = path[0];
const nextPath = path.slice(1).join('.');
object[firstKey] = expandResultOnKeyPath(object[firstKey] || {}, nextPath, value[firstKey]);
delete object[key];
return object;
}

function sanitizeDatabaseResult(originalObject, result) {
const response = {};
if (!result) {
Expand All @@ -319,7 +332,8 @@ function sanitizeDatabaseResult(originalObject, result) {
if (keyUpdate && typeof keyUpdate === 'object' && keyUpdate.__op
&& ['Add', 'AddUnique', 'Remove', 'Increment'].indexOf(keyUpdate.__op) > -1) {
// only valid ops that produce an actionable result
response[key] = result[key];
// the op may have happend on a keypath
expandResultOnKeyPath(response, key, result);
}
});
return Promise.resolve(response);
Expand Down
2 changes: 1 addition & 1 deletion src/Push/PushWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export class PushWorker {
const auth = master(config);
const where = utils.applyDeviceTokenExists(query.where);
delete query.where;
pushStatus = pushStatusHandler(config, pushStatus.objectId);
return rest.find(config, auth, '_Installation', where, query).then(({results}) => {
if (results.length == 0) {
return;
Expand All @@ -63,7 +64,6 @@ export class PushWorker {
}

sendToAdapter(body: any, installations: any[], pushStatus: any, config: Config, UTCOffset: ?any): Promise<*> {
pushStatus = pushStatusHandler(config, pushStatus.objectId);
// Check if we have locales in the push body
const locales = utils.getLocalesFromPush(body);
if (locales.length > 0) {
Expand Down
78 changes: 63 additions & 15 deletions src/StatusHandler.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { md5Hash, newObjectId } from './cryptoUtils';
import { logger } from './logger';
import Parse from 'parse/node';

const PUSH_STATUS_COLLECTION = '_PushStatus';
const JOB_STATUS_COLLECTION = '_JobStatus';
Expand Down Expand Up @@ -50,6 +51,47 @@ function statusHandler(className, database) {
})
}

function restStatusHandler(className) {
let lastPromise = Promise.resolve();

function create(object) {
lastPromise = lastPromise.then(() => {
return Parse._request(
'POST',
`classes/${className}`,
object,
{ useMasterKey: true }
).then((result) => {
// merge the objects
const response = Object.assign({}, object, result);
return Promise.resolve(response);
});
});
return lastPromise;
}

function update(where, object) {
// TODO: when we have updateWhere, use that for proper interfacing
lastPromise = lastPromise.then(() => {
return Parse._request(
'PUT',
`classes/${className}/${where.objectId}`,
object,
{ useMasterKey: true }).then((result) => {
// merge the objects
const response = Object.assign({}, object, result);
return Promise.resolve(response);
});
});
return lastPromise;
}

return Object.freeze({
create,
update
})
}

export function jobStatusHandler(config) {
let jobStatus;
const objectId = newObjectId(config.objectIdSize);
Expand Down Expand Up @@ -103,11 +145,12 @@ export function jobStatusHandler(config) {
});
}

export function pushStatusHandler(config, objectId = newObjectId(config.objectIdSize)) {
export function pushStatusHandler(config, existingObjectId) {

let pushStatus;
const database = config.database;
const handler = statusHandler(PUSH_STATUS_COLLECTION, database);
const handler = restStatusHandler(PUSH_STATUS_COLLECTION);
let objectId = existingObjectId;
const setInitial = function(body = {}, where, options = {source: 'rest'}) {
const now = new Date();
let pushTime = now.toISOString();
Expand All @@ -133,8 +176,6 @@ export function pushStatusHandler(config, objectId = newObjectId(config.objectId
pushHash = 'd41d8cd98f00b204e9800998ecf8427e';
}
const object = {
objectId,
createdAt: now,
pushTime,
query: JSON.stringify(where),
payload: payloadString,
Expand All @@ -148,7 +189,8 @@ export function pushStatusHandler(config, objectId = newObjectId(config.objectId
ACL: {}
}

return handler.create(object).then(() => {
return handler.create(object).then((result) => {
objectId = result.objectId;
pushStatus = {
objectId
};
Expand All @@ -159,12 +201,11 @@ export function pushStatusHandler(config, objectId = newObjectId(config.objectId
const setRunning = function(count) {
logger.verbose(`_PushStatus ${objectId}: sending push to %d installations`, count);
return handler.update({status:"pending", objectId: objectId},
{status: "running", updatedAt: new Date(), count });
{status: "running", count });
}

const trackSent = function(results, UTCOffset, cleanupInstallations = process.env.PARSE_SERVER_CLEANUP_INVALID_INSTALLATIONS) {
const update = {
updatedAt: new Date(),
numSent: 0,
numFailed: 0
};
Expand Down Expand Up @@ -236,27 +277,34 @@ export function pushStatusHandler(config, objectId = newObjectId(config.objectId
const complete = function() {
return handler.update({ objectId }, {
status: 'succeeded',
count: {__op: 'Delete'},
updatedAt: new Date()
count: {__op: 'Delete'}
});
}

const fail = function(err) {
if (typeof err === 'string') {
err = { message: err };
}
const update = {
errorMessage: JSON.stringify(err),
status: 'failed',
updatedAt: new Date()
errorMessage: err,
status: 'failed'
}
logger.warn(`_PushStatus ${objectId}: error while sending push`, err);
return handler.update({ objectId }, update);
}

return Object.freeze({
objectId,
const rval = {
setInitial,
setRunning,
trackSent,
complete,
fail
})
};

// define objectId to be dynamic
Object.defineProperty(rval, "objectId", {
get: () => objectId
});

return Object.freeze(rval);
}
Loading