From 7c387e1ee9fef29cd3d57ced706708088ec26247 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Sun, 13 Mar 2016 23:34:44 -0400 Subject: [PATCH] Adds support to store push results --- spec/PushController.spec.js | 77 +++++++++++++++------ src/Adapters/Push/ParsePushAdapter.js | 5 +- src/Controllers/PushController.js | 96 ++++++++++++--------------- src/Schema.js | 4 +- src/pushStatusHandler.js | 76 +++++++++++++++++++++ 5 files changed, 183 insertions(+), 75 deletions(-) create mode 100644 src/pushStatusHandler.js diff --git a/spec/PushController.spec.js b/spec/PushController.spec.js index 317678c1..f776fb9d 100644 --- a/spec/PushController.spec.js +++ b/spec/PushController.spec.js @@ -3,6 +3,30 @@ var PushController = require('../src/Controllers/PushController').PushController var Config = require('../src/Config'); +const successfulTransmissions = function(body, installations) { + + let promises = installations.map((device) => { + return Promise.resolve({ + transmitted: true, + device: device, + }) + }); + + return Promise.all(promises); +} + +const successfulIOS = function(body, installations) { + + let promises = installations.map((device) => { + return Promise.resolve({ + transmitted: device.deviceType == "ios", + device: device, + }) + }); + + return Promise.all(promises); +} + describe('PushController', () => { it('can validate device type when no device type is set', (done) => { // Make query condition @@ -142,10 +166,7 @@ describe('PushController', () => { expect(installation.badge).toBeUndefined(); } }) - return Promise.resolve({ - error: null, - payload: body, - }) + return successfulTransmissions(body, installations); }, getValidPushTypes: function() { return ["ios", "android"]; @@ -194,10 +215,7 @@ describe('PushController', () => { expect(installation.badge).toEqual(badge); expect(1).toEqual(installation.badge); }) - return Promise.resolve({ - payload: body, - error: null - }) + return successfulTransmissions(body, installations); }, getValidPushTypes: function() { return ["ios"]; @@ -224,6 +242,24 @@ describe('PushController', () => { it('properly creates _PushStatus', (done) => { + var installations = []; + while(installations.length != 10) { + var 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); + } + + while(installations.length != 15) { + var installation = new Parse.Object("_Installation"); + installation.set("installationId", "installation_"+installations.length); + installation.set("deviceToken","device_token_"+installations.length) + installation.set("deviceType", "android"); + installations.push(installation); + } var payload = {data: { alert: "Hello World!", badge: 1, @@ -231,12 +267,7 @@ describe('PushController', () => { var pushAdapter = { send: function(body, installations) { - var badge = body.data.badge; - return Promise.resolve({ - error: null, - response: "OK!", - payload: body - }); + return successfulIOS(body, installations); }, getValidPushTypes: function() { return ["ios"]; @@ -249,7 +280,9 @@ describe('PushController', () => { } var pushController = new PushController(pushAdapter, Parse.applicationId); - pushController.sendPush(payload, {}, config, auth).then((result) => { + Parse.Object.saveAll(installations).then(() => { + return pushController.sendPush(payload, {}, config, auth); + }).then((result) => { let query = new Parse.Query('_PushStatus'); return query.find({useMasterKey: true}); }).then((results) => { @@ -258,7 +291,15 @@ describe('PushController', () => { expect(result.get('source')).toEqual('rest'); expect(result.get('query')).toEqual(JSON.stringify({})); expect(result.get('payload')).toEqual(payload.data); - expect(result.get('status')).toEqual("running"); + expect(result.get('status')).toEqual('succeeded'); + expect(result.get('numSent')).toEqual(10); + expect(result.get('sentPerType')).toEqual({ + 'ios': 10 // 10 ios + }); + expect(result.get('numFailed')).toEqual(5); + expect(result.get('failedPerType')).toEqual({ + 'android': 5 // android + }); done(); }); @@ -272,9 +313,7 @@ describe('PushController', () => { var pushAdapter = { send: function(body, installations) { - return Promise.resolve({ - error:null - }); + return successfulTransmissions(body, installations); }, getValidPushTypes: function() { return ["ios"]; diff --git a/src/Adapters/Push/ParsePushAdapter.js b/src/Adapters/Push/ParsePushAdapter.js index 7b3cfa03..72cd57ed 100644 --- a/src/Adapters/Push/ParsePushAdapter.js +++ b/src/Adapters/Push/ParsePushAdapter.js @@ -10,6 +10,9 @@ import PushAdapter from './PushAdapter'; import { classifyInstallations } from './PushAdapterUtils'; export class ParsePushAdapter extends PushAdapter { + + supportsPushTracking = true; + constructor(pushConfig = {}) { super(pushConfig); this.validPushTypes = ['ios', 'android']; @@ -56,7 +59,7 @@ export class ParsePushAdapter extends PushAdapter { })) } else { let devices = deviceMap[pushType]; - sendPromises.push(sender.send(data, devices)); + sendPromises.push(sender.send(data, devices)); } } return Parse.Promise.when(sendPromises); diff --git a/src/Controllers/PushController.js b/src/Controllers/PushController.js index 1c5edc89..efe9a750 100644 --- a/src/Controllers/PushController.js +++ b/src/Controllers/PushController.js @@ -4,10 +4,9 @@ import rest from '../rest'; import AdaptableController from './AdaptableController'; import { PushAdapter } from '../Adapters/Push/PushAdapter'; import deepcopy from 'deepcopy'; -import { md5Hash } from '../cryptoUtils'; import features from '../features'; import RestQuery from '../RestQuery'; -import RestWrite from '../RestWrite'; +import pushStatusHandler from '../pushStatusHandler'; const FEATURE_NAME = 'push'; const UNSUPPORTED_BADGE_KEY = "unsupported"; @@ -40,7 +39,7 @@ export class PushController extends AdaptableController { } } - sendPush(body = {}, where = {}, config, auth) { + sendPush(body = {}, where = {}, config, auth, wait) { var pushAdapter = this.adapter; if (!pushAdapter) { throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, @@ -83,67 +82,56 @@ export class PushController extends AdaptableController { }) } } - let pushStatus; + let pushStatus = pushStatusHandler(config); return Promise.resolve().then(() => { - return this.saveInitialPushStatus(body, where, config); - }).then((res) => { - pushStatus = res.response; + return pushStatus.setInitial(body, where); + }).then(() => { return badgeUpdate(); }).then(() => { return rest.find(config, auth, '_Installation', where); }).then((response) => { - this.updatePushStatus({status: "running"}, {status:"pending", objectId: pushStatus.objectId}, config); - if (body.data && body.data.badge && body.data.badge == "Increment") { - // Collect the badges to reduce the # of calls - let badgeInstallationsMap = response.results.reduce((map, installation) => { - let badge = installation.badge; - if (installation.deviceType != "ios") { - badge = UNSUPPORTED_BADGE_KEY; - } - map[badge+''] = map[badge+''] || []; - map[badge+''].push(installation); - return map; - }, {}); - - // Map the on the badges count and return the send result - let promises = Object.keys(badgeInstallationsMap).map((badge) => { - let payload = deepcopy(body); - if (badge == UNSUPPORTED_BADGE_KEY) { - delete payload.data.badge; - } else { - payload.data.badge = parseInt(badge); - } - return pushAdapter.send(payload, badgeInstallationsMap[badge], pushStatus); - }); - return Promise.all(promises); - } - return pushAdapter.send(body, response.results, pushStatus); + pushStatus.setRunning(); + return this.sendToAdapter(body, response.results, pushStatus, config); }).then((results) => { - // TODO: handle push results - return Promise.resolve(results); + return pushStatus.complete(results); }); } - saveInitialPushStatus(body, where, config, options = {source: 'rest'}) { - let pushStatus = { - pushTime: (new Date()).toISOString(), - query: JSON.stringify(where), - payload: body.data, - source: options.source, - title: options.title, - expiry: body.expiration_time, - status: "pending", - numSent: 0, - pushHash: md5Hash(JSON.stringify(body.data)), - ACL: new Parse.ACL() // lockdown! - } - let restWrite = new RestWrite(config, {isMaster: true},'_PushStatus',null, pushStatus); - return restWrite.execute(); - } + sendToAdapter(body, installations, pushStatus, config) { + if (body.data && body.data.badge && body.data.badge == "Increment") { + // Collect the badges to reduce the # of calls + let badgeInstallationsMap = installations.reduce((map, installation) => { + let badge = installation.badge; + if (installation.deviceType != "ios") { + badge = UNSUPPORTED_BADGE_KEY; + } + map[badge+''] = map[badge+''] || []; + map[badge+''].push(installation); + return map; + }, {}); - updatePushStatus(update, where, config) { - let restWrite = new RestWrite(config, {isMaster: true}, '_PushStatus', where, update); - return restWrite.execute(); + // Map the on the badges count and return the send result + let promises = Object.keys(badgeInstallationsMap).map((badge) => { + let payload = deepcopy(body); + if (badge == UNSUPPORTED_BADGE_KEY) { + delete payload.data.badge; + } else { + payload.data.badge = parseInt(badge); + } + return this.adapter.send(payload, badgeInstallationsMap[badge]); + }); + // Flatten the promises results + return Promise.all(promises).then((results) => { + if (Array.isArray(results)) { + return Promise.resolve(results.reduce((memo, result) => { + return memo.concat(result); + },[])); + } else { + return Promise.resolve(results); + } + }) + } + return this.adapter.send(body, installations); } /** diff --git a/src/Schema.js b/src/Schema.js index 2b3e2e39..f05aced0 100644 --- a/src/Schema.js +++ b/src/Schema.js @@ -78,9 +78,11 @@ var defaultColumns = { "expiry": {type:'Number'}, "status": {type:'String'}, "numSent": {type:'Number'}, + "numFailed": {type:'Number'}, "pushHash": {type:'String'}, "errorMessage": {type:'Object'}, - "sentPerType": {type:'Object'} + "sentPerType": {type:'Object'}, + "failedPerType":{type:'Object'}, } }; diff --git a/src/pushStatusHandler.js b/src/pushStatusHandler.js new file mode 100644 index 00000000..362ddb76 --- /dev/null +++ b/src/pushStatusHandler.js @@ -0,0 +1,76 @@ +import RestWrite from './RestWrite'; +import { md5Hash } from './cryptoUtils'; + +export default function pushStatusHandler(config) { + + let initialPromise; + let pushStatus; + let setInitial = function(body, where, options = {source: 'rest'}) { + let object = { + pushTime: (new Date()).toISOString(), + query: JSON.stringify(where), + payload: body.data, + source: options.source, + title: options.title, + expiry: body.expiration_time, + status: "pending", + numSent: 0, + pushHash: md5Hash(JSON.stringify(body.data)), + ACL: new Parse.ACL() // lockdown! + } + let restWrite = new RestWrite(config, {isMaster: true},'_PushStatus',null, object); + initialPromise = restWrite.execute().then((res) => { + pushStatus = res.response; + return Promise.resolve(pushStatus); + }); + return initialPromise; + } + + let setRunning = function() { + return initialPromise.then(() => { + let restWrite = new RestWrite(config, {isMaster: true}, '_PushStatus', {status:"pending", objectId: pushStatus.objectId}, {status: "running"}); + return restWrite.execute(); + }) + } + + let complete = function(results) { + let update = { + status: 'succeeded', + numSent: 0, + numFailed: 0, + }; + if (Array.isArray(results)) { + results.reduce((memo, result) => { + // Cannot handle that + if (!result.device || !result.device.deviceType) { + return memo; + } + let deviceType = result.device.deviceType; + if (result.transmitted) + { + memo.numSent++; + memo.sentPerType = memo.sentPerType || {}; + memo.sentPerType[deviceType] = memo.sentPerType[deviceType] || 0; + memo.sentPerType[deviceType]++; + } else { + memo.numFailed++; + memo.failedPerType = memo.failedPerType || {}; + memo.failedPerType[deviceType] = memo.failedPerType[deviceType] || 0; + memo.failedPerType[deviceType]++; + } + return memo; + }, update); + } + + return initialPromise.then(() => { + let restWrite = new RestWrite(config, {isMaster: true}, '_PushStatus', {status:"running", objectId: pushStatus.objectId}, update); + return restWrite.execute(); + }) + } + + return Object.freeze({ + setInitial, + setRunning, + complete + }) +}