From 4dce3bd63cf4c30b011cb9bd995b208606a07bef Mon Sep 17 00:00:00 2001 From: Anthony Mosca Date: Tue, 12 Sep 2017 11:36:21 +0930 Subject: [PATCH] Add support for more audience fields. (#4145) * Add support for more audience fields. * Only update audience when defined audience_id. --- spec/AudienceRouter.spec.js | 30 +++++-- spec/PushController.spec.js | 89 ++++++++++++++++++++ spec/helper.js | 2 +- src/Adapters/Storage/Mongo/MongoTransform.js | 32 ++++++- src/Controllers/PushController.js | 14 +++ src/Controllers/SchemaController.js | 10 ++- 6 files changed, 160 insertions(+), 17 deletions(-) diff --git a/spec/AudienceRouter.spec.js b/spec/AudienceRouter.spec.js index 0acaac1b..c3c6c7ad 100644 --- a/spec/AudienceRouter.spec.js +++ b/spec/AudienceRouter.spec.js @@ -285,7 +285,7 @@ describe('AudiencesRouter', () => { ); }); - it_exclude_dbs(['postgres'])('should not log error with legacy parse.com times_used and _last_used fields', (done) => { + it_exclude_dbs(['postgres'])('should support legacy parse.com audience fields', (done) => { const database = (new Config(Parse.applicationId)).database.adapter.database; const now = new Date(); Parse._request('POST', 'push_audiences', { name: 'My Audience', query: JSON.stringify({ deviceType: 'ios' })}, { useMasterKey: true }) @@ -306,13 +306,12 @@ describe('AudiencesRouter', () => { expect(error).toEqual(null) expect(rows[0]['times_used']).toEqual(1); expect(rows[0]['_last_used']).toEqual(now); - Parse._request('GET', 'push_audiences', {}, {useMasterKey: true}) - .then((results) => { - expect(results.results.length).toEqual(1); - expect(results.results[0].name).toEqual('My Audience'); - expect(results.results[0].query.deviceType).toEqual('ios'); - expect(results.results[0].times_used).toEqual(undefined); - expect(results.results[0]._last_used).toEqual(undefined); + Parse._request('GET', 'push_audiences/' + audience.objectId, {}, {useMasterKey: true}) + .then((audience) => { + expect(audience.name).toEqual('My Audience'); + expect(audience.query.deviceType).toEqual('ios'); + expect(audience.timesUsed).toEqual(1); + expect(audience.lastUsed).toEqual(now.toISOString()); done(); }) .catch((error) => { done.fail(error); }) @@ -320,4 +319,19 @@ describe('AudiencesRouter', () => { }); }); }); + + it('should be able to search on audiences', (done) => { + Parse._request('POST', 'push_audiences', { name: 'neverUsed', query: JSON.stringify({ deviceType: 'ios' })}, { useMasterKey: true }) + .then(() => { + const query = {"timesUsed": {"$exists": false}, "lastUsed": {"$exists": false}}; + Parse._request('GET', 'push_audiences?order=-createdAt&limit=1', {where: query}, {useMasterKey: true}) + .then((results) => { + expect(results.results.length).toEqual(1); + const audience = results.results[0]; + expect(audience.name).toEqual("neverUsed"); + done(); + }) + .catch((error) => { done.fail(error); }) + }) + }); }); diff --git a/spec/PushController.spec.js b/spec/PushController.spec.js index b6a14d32..70eca6d1 100644 --- a/spec/PushController.spec.js +++ b/spec/PushController.spec.js @@ -1000,6 +1000,95 @@ describe('PushController', () => { }).catch(done.fail); }); + it('should update audiences', (done) => { + var pushAdapter = { + send: function(body, installations) { + return successfulTransmissions(body, installations); + }, + getValidPushTypes: function() { + return ["ios"]; + } + } + + var config = new Config(Parse.applicationId); + var auth = { + isMaster: true + } + + var audienceId = null; + var now = new Date(); + var timesUsed = 0; + + const where = { + 'deviceType': 'ios' + } + spyOn(pushAdapter, 'send').and.callThrough(); + var pushController = new PushController(); + reconfigureServer({ + push: { adapter: pushAdapter } + }).then(() => { + var installations = []; + while (installations.length != 5) { + 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); + } + return Parse.Object.saveAll(installations); + }).then(() => { + // Create an audience + const query = new Parse.Query("_Audience"); + query.descending("createdAt"); + query.equalTo("query", JSON.stringify(where)); + const parseResults = (results) => { + if (results.length > 0) { + audienceId = results[0].id; + timesUsed = results[0].get('timesUsed'); + if (!isFinite(timesUsed)) { + timesUsed = 0; + } + } + } + const audience = new Parse.Object("_Audience"); + audience.set("name", "testAudience") + audience.set("query", JSON.stringify(where)); + return Parse.Object.saveAll(audience).then(() => { + return query.find({ useMasterKey: true }).then(parseResults); + }); + }).then(() => { + var body = { + data: { alert: 'hello' }, + audience_id: audienceId + } + return pushController.sendPush(body, where, config, auth) + }).then(() => { + // Wait so the push is completed. + return new Promise((resolve) => { setTimeout(() => { resolve(); }, 1000); }); + }).then(() => { + expect(pushAdapter.send.calls.count()).toBe(1); + const firstCall = pushAdapter.send.calls.first(); + expect(firstCall.args[0].data).toEqual({ + alert: 'hello' + }); + expect(firstCall.args[1].length).toBe(5); + }).then(() => { + // Get the audience we used above. + const query = new Parse.Query("_Audience"); + query.equalTo("objectId", audienceId); + return query.find({ useMasterKey: true }) + }).then((results) => { + const audience = results[0]; + expect(audience.get('query')).toBe(JSON.stringify(where)); + expect(audience.get('timesUsed')).toBe(timesUsed + 1); + expect(audience.get('lastUsed')).not.toBeLessThan(now); + }).then(() => { + done(); + }).catch(done.fail); + }); + describe('pushTimeHasTimezoneComponent', () => { it('should be accurate', () => { expect(PushController.pushTimeHasTimezoneComponent('2017-09-06T17:14:01.048Z')) diff --git a/spec/helper.js b/spec/helper.js index 60a929a5..d2482224 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -212,7 +212,7 @@ afterEach(function(done) { } else { // Other system classes will break Parse.com, so make sure that we don't save anything to _SCHEMA that will // break it. - return ['_User', '_Installation', '_Role', '_Session', '_Product'].indexOf(className) >= 0; + return ['_User', '_Installation', '_Role', '_Session', '_Product', '_Audience'].indexOf(className) >= 0; } }}); }); diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 61510287..143df558 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -10,6 +10,8 @@ const transformKey = (className, fieldName, schema) => { case 'createdAt': return '_created_at'; case 'updatedAt': return '_updated_at'; case 'sessionToken': return '_session_token'; + case 'lastUsed': return '_last_used'; + case 'timesUsed': return 'times_used'; } if (schema.fields[fieldName] && schema.fields[fieldName].__type == 'Pointer') { @@ -77,6 +79,16 @@ const transformKeyValueForUpdate = (className, restKey, restValue, parseFormatSc case '_rperm': case '_wperm': return {key: key, value: restValue}; + case 'lastUsed': + case '_last_used': + key = '_last_used'; + timeField = true; + break; + case 'timesUsed': + case 'times_used': + key = 'times_used'; + timeField = true; + break; } if ((parseFormatSchema.fields[key] && parseFormatSchema.fields[key].type === 'Pointer') || (!parseFormatSchema.fields[key] && restValue && restValue.__type == 'Pointer')) { @@ -200,6 +212,14 @@ function transformQueryKeyValue(className, key, value, schema) { return {key: '$or', value: value.map(subQuery => transformWhere(className, subQuery, schema))}; case '$and': return {key: '$and', value: value.map(subQuery => transformWhere(className, subQuery, schema))}; + case 'lastUsed': + if (valueAsDate(value)) { + return {key: '_last_used', value: valueAsDate(value)} + } + key = '_last_used'; + break; + case 'timesUsed': + return {key: 'times_used', value: value}; default: { // Other auth data const authDataMatch = key.match(/^authData\.([a-zA-Z0-9_]+)\.id$/); @@ -923,11 +943,15 @@ const mongoObjectToParseObject = (className, mongoObject, schema) => { case '_expiresAt': restObject['expiresAt'] = Parse._encode(new Date(mongoObject[key])); break; + case 'lastUsed': + case '_last_used': + restObject['lastUsed'] = Parse._encode(new Date(mongoObject[key])).iso; + break; + case 'timesUsed': + case 'times_used': + restObject['timesUsed'] = mongoObject[key]; + break; default: - if (className === '_Audience' && (key === '_last_used' || key === 'times_used')) { - // Ignore these parse.com legacy fields - break; - } // Check other auth data keys var authDataMatch = key.match(/^_auth_data_([a-zA-Z0-9_]+)$/); if (authDataMatch) { diff --git a/src/Controllers/PushController.js b/src/Controllers/PushController.js index 0a3efed4..73eb7c48 100644 --- a/src/Controllers/PushController.js +++ b/src/Controllers/PushController.js @@ -54,6 +54,20 @@ export class PushController { }).then(() => { onPushStatusSaved(pushStatus.objectId); return badgeUpdate(); + }).then(() => { + // Update audience lastUsed and timesUsed + if (body.audience_id) { + const audienceId = body.audience_id; + + var updateAudience = { + lastUsed: { __type: "Date", iso: new Date().toISOString() }, + timesUsed: { __op: "Increment", "amount": 1 } + }; + const write = new RestWrite(config, master(config), '_Audience', {objectId: audienceId}, updateAudience); + write.execute(); + } + // Don't wait for the audience update promise to resolve. + return Promise.resolve(); }).then(() => { if (body.hasOwnProperty('push_time') && config.hasPushScheduledSupport) { return Promise.resolve(); diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index 86ae1d55..e61f30a2 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -113,12 +113,14 @@ const defaultColumns = Object.freeze({ }, _GlobalConfig: { "objectId": {type: 'String'}, - "params": {type: 'Object'} + "params": {type: 'Object'} }, _Audience: { - "objectId": {type:'String'}, - "name": {type:'String'}, - "query": {type:'String'} //storing query as JSON string to prevent "Nested keys should not contain the '$' or '.' characters" error + "objectId": {type:'String'}, + "name": {type:'String'}, + "query": {type:'String'}, //storing query as JSON string to prevent "Nested keys should not contain the '$' or '.' characters" error + "lastUsed": {type:'Date'}, + "timesUsed": {type:'Number'} } });