Improves Controller and Adapter relationship

- Controllers that have adapters are AdaptableControllers
- AdaptableController is responsible to instantiate the proper adapter if needed (string, function or BaseAdapter)
- BaseAdapter is the base class for adapters, allows skipping when passed directly to the controller
This commit is contained in:
Florent Vilmart
2016-02-21 12:02:18 -05:00
parent bd548786ea
commit d504681589
11 changed files with 403 additions and 319 deletions

View File

@@ -0,0 +1,68 @@
var AdaptableController = require("../src/Controllers/AdaptableController").AdaptableController;
var FilesAdapter = require("../src/Adapters/Files/FilesAdapter").default;
describe("AdaptableController", ()=>{
it("should instantiate an adapter from string in object", (done) => {
var adapterPath = require('path').resolve("./spec/MockAdapter");
var controller = new AdaptableController({
adapter: adapterPath,
key: "value",
foo: "bar"
});
expect(controller.adapter instanceof Object).toBe(true);
expect(controller.options.key).toBe("value");
expect(controller.options.foo).toBe("bar");
expect(controller.adapter.options.key).toBe("value");
expect(controller.adapter.options.foo).toBe("bar");
done();
});
it("should instantiate an adapter from string", (done) => {
var adapterPath = require('path').resolve("./spec/MockAdapter");
var controller = new AdaptableController(adapterPath);
expect(controller.adapter instanceof Object).toBe(true);
done();
});
it("should instantiate an adapter from string that is module", (done) => {
var adapterPath = require('path').resolve("./src/Adapters/Files/FilesAdapter");
var controller = new AdaptableController({
adapter: adapterPath
});
expect(controller.adapter instanceof FilesAdapter).toBe(true);
done();
});
it("should instantiate an adapter from function/Class", (done) => {
var controller = new AdaptableController({
adapter: FilesAdapter
});
expect(controller.adapter instanceof FilesAdapter).toBe(true);
done();
});
it("should instantiate the default adapter from Class", (done) => {
var controller = new AdaptableController(null, FilesAdapter);
expect(controller.adapter instanceof FilesAdapter).toBe(true);
done();
});
it("should use the default adapter", (done) => {
var adapter = new FilesAdapter();
var controller = new AdaptableController(null, adapter);
expect(controller.adapter).toBe(adapter);
done();
});
it("should use the provided adapter", (done) => {
var adapter = new FilesAdapter();
var controller = new AdaptableController(adapter);
expect(controller.adapter).toBe(adapter);
done();
});
});

3
spec/MockAdapter.js Normal file
View File

@@ -0,0 +1,3 @@
module.exports = function(options) {
this.options = options;
}

View File

@@ -227,7 +227,8 @@ describe('OneSignalPushAdapter', () => {
function makeDevice(deviceToken, appIdentifier) {
return {
deviceToken: deviceToken
deviceToken: deviceToken,
appIdentifier: appIdentifier
};
}

View File

@@ -5,226 +5,191 @@
const Parse = require('parse/node').Parse;
var deepcopy = require('deepcopy');
import PushAdapter from './PushAdapter';
function OneSignalPushAdapter(pushConfig) {
this.https = require('https');
this.validPushTypes = ['ios', 'android'];
this.senderMap = {};
pushConfig = pushConfig || {};
this.OneSignalConfig = {};
this.OneSignalConfig['appId'] = pushConfig['oneSignalAppId'];
this.OneSignalConfig['apiKey'] = pushConfig['oneSignalApiKey'];
export class OneSignalPushAdapter extends PushAdapter {
this.senderMap['ios'] = this.sendToAPNS.bind(this);
this.senderMap['android'] = this.sendToGCM.bind(this);
}
/**
* Get an array of valid push types.
* @returns {Array} An array of valid push types
*/
OneSignalPushAdapter.prototype.getValidPushTypes = function() {
return this.validPushTypes;
}
OneSignalPushAdapter.prototype.send = function(data, installations) {
console.log("Sending notification to "+installations.length+" devices.")
let deviceMap = classifyInstallation(installations, this.validPushTypes);
let sendPromises = [];
for (let pushType in deviceMap) {
let sender = this.senderMap[pushType];
if (!sender) {
console.log('Can not find sender for push type %s, %j', pushType, data);
continue;
}
let devices = deviceMap[pushType];
if(devices.length > 0) {
sendPromises.push(sender(data, devices));
}
constructor(pushConfig = {}) {
super(pushConfig);
this.https = require('https');
this.validPushTypes = ['ios', 'android'];
this.senderMap = {};
this.OneSignalConfig = {};
this.OneSignalConfig['appId'] = pushConfig['oneSignalAppId'];
this.OneSignalConfig['apiKey'] = pushConfig['oneSignalApiKey'];
this.senderMap['ios'] = this.sendToAPNS.bind(this);
this.senderMap['android'] = this.sendToGCM.bind(this);
}
return Parse.Promise.when(sendPromises);
}
OneSignalPushAdapter.prototype.sendToAPNS = function(data,tokens) {
data= deepcopy(data['data']);
var post = {};
if(data['badge']) {
if(data['badge'] == "Increment") {
post['ios_badgeType'] = 'Increase';
post['ios_badgeCount'] = 1;
} else {
post['ios_badgeType'] = 'SetTo';
post['ios_badgeCount'] = data['badge'];
}
delete data['badge'];
}
if(data['alert']) {
post['contents'] = {en: data['alert']};
delete data['alert'];
}
if(data['sound']) {
post['ios_sound'] = data['sound'];
delete data['sound'];
}
if(data['content-available'] == 1) {
post['content_available'] = true;
delete data['content-available'];
}
post['data'] = data;
let promise = new Parse.Promise();
var chunk = 2000 // OneSignal can process 2000 devices at a time
var tokenlength=tokens.length;
var offset = 0
// handle onesignal response. Start next batch if there's not an error.
let handleResponse = function(wasSuccessful) {
if (!wasSuccessful) {
return promise.reject("OneSignal Error");
}
if(offset >= tokenlength) {
promise.resolve()
} else {
this.sendNext();
}
}.bind(this)
this.sendNext = function() {
post['include_ios_tokens'] = [];
tokens.slice(offset,offset+chunk).forEach(function(i) {
post['include_ios_tokens'].push(i['deviceToken'])
})
offset+=chunk;
this.sendToOneSignal(post, handleResponse);
}.bind(this)
this.sendNext()
return promise;
}
OneSignalPushAdapter.prototype.sendToGCM = function(data,tokens) {
data= deepcopy(data['data']);
var post = {};
if(data['alert']) {
post['contents'] = {en: data['alert']};
delete data['alert'];
}
if(data['title']) {
post['title'] = {en: data['title']};
delete data['title'];
}
if(data['uri']) {
post['url'] = data['uri'];
}
send(data, installations) {
console.log("Sending notification to "+installations.length+" devices.")
let deviceMap = PushAdapter.classifyInstallation(installations, this.validPushTypes);
post['data'] = data;
let sendPromises = [];
for (let pushType in deviceMap) {
let sender = this.senderMap[pushType];
if (!sender) {
console.log('Can not find sender for push type %s, %j', pushType, data);
continue;
}
let devices = deviceMap[pushType];
let promise = new Parse.Promise();
if(devices.length > 0) {
sendPromises.push(sender(data, devices));
}
}
return Parse.Promise.when(sendPromises);
}
sendToAPNS(data,tokens) {
var chunk = 2000 // OneSignal can process 2000 devices at a time
var tokenlength=tokens.length;
var offset = 0
// handle onesignal response. Start next batch if there's not an error.
let handleResponse = function(wasSuccessful) {
if (!wasSuccessful) {
return promise.reject("OneSIgnal Error");
data= deepcopy(data['data']);
var post = {};
if(data['badge']) {
if(data['badge'] == "Increment") {
post['ios_badgeType'] = 'Increase';
post['ios_badgeCount'] = 1;
} else {
post['ios_badgeType'] = 'SetTo';
post['ios_badgeCount'] = data['badge'];
}
delete data['badge'];
}
if(data['alert']) {
post['contents'] = {en: data['alert']};
delete data['alert'];
}
if(data['sound']) {
post['ios_sound'] = data['sound'];
delete data['sound'];
}
if(data['content-available'] == 1) {
post['content_available'] = true;
delete data['content-available'];
}
post['data'] = data;
let promise = new Parse.Promise();
var chunk = 2000 // OneSignal can process 2000 devices at a time
var tokenlength=tokens.length;
var offset = 0
// handle onesignal response. Start next batch if there's not an error.
let handleResponse = function(wasSuccessful) {
if (!wasSuccessful) {
return promise.reject("OneSignal Error");
}
if(offset >= tokenlength) {
promise.resolve()
} else {
this.sendNext();
}
}.bind(this)
this.sendNext = function() {
post['include_ios_tokens'] = [];
tokens.slice(offset,offset+chunk).forEach(function(i) {
post['include_ios_tokens'].push(i['deviceToken'])
})
offset+=chunk;
this.sendToOneSignal(post, handleResponse);
}.bind(this)
this.sendNext()
return promise;
}
sendToGCM(data,tokens) {
data= deepcopy(data['data']);
var post = {};
if(data['alert']) {
post['contents'] = {en: data['alert']};
delete data['alert'];
}
if(data['title']) {
post['title'] = {en: data['title']};
delete data['title'];
}
if(data['uri']) {
post['url'] = data['uri'];
}
if(offset >= tokenlength) {
promise.resolve()
} else {
this.sendNext();
}
}.bind(this);
post['data'] = data;
this.sendNext = function() {
post['include_android_reg_ids'] = [];
tokens.slice(offset,offset+chunk).forEach(function(i) {
post['include_android_reg_ids'].push(i['deviceToken'])
})
offset+=chunk;
this.sendToOneSignal(post, handleResponse);
}.bind(this)
let promise = new Parse.Promise();
var chunk = 2000 // OneSignal can process 2000 devices at a time
var tokenlength=tokens.length;
var offset = 0
// handle onesignal response. Start next batch if there's not an error.
let handleResponse = function(wasSuccessful) {
if (!wasSuccessful) {
return promise.reject("OneSIgnal Error");
}
if(offset >= tokenlength) {
promise.resolve()
} else {
this.sendNext();
}
}.bind(this);
this.sendNext = function() {
post['include_android_reg_ids'] = [];
tokens.slice(offset,offset+chunk).forEach(function(i) {
post['include_android_reg_ids'].push(i['deviceToken'])
})
offset+=chunk;
this.sendToOneSignal(post, handleResponse);
}.bind(this)
this.sendNext();
return promise;
this.sendNext();
return promise;
}
sendToOneSignal(data, cb) {
let headers = {
"Content-Type": "application/json",
"Authorization": "Basic "+this.OneSignalConfig['apiKey']
};
let options = {
host: "onesignal.com",
port: 443,
path: "/api/v1/notifications",
method: "POST",
headers: headers
};
data['app_id'] = this.OneSignalConfig['appId'];
let request = this.https.request(options, function(res) {
if(res.statusCode < 299) {
cb(true);
} else {
console.log('OneSignal Error');
res.on('data', function(chunk) {
console.log(chunk.toString())
});
cb(false)
}
});
request.on('error', function(e) {
console.log("Error connecting to OneSignal")
console.log(e);
cb(false);
});
request.write(JSON.stringify(data))
request.end();
}
}
OneSignalPushAdapter.prototype.sendToOneSignal = function(data, cb) {
let headers = {
"Content-Type": "application/json",
"Authorization": "Basic "+this.OneSignalConfig['apiKey']
};
let options = {
host: "onesignal.com",
port: 443,
path: "/api/v1/notifications",
method: "POST",
headers: headers
};
data['app_id'] = this.OneSignalConfig['appId'];
let request = this.https.request(options, function(res) {
if(res.statusCode < 299) {
cb(true);
} else {
console.log('OneSignal Error');
res.on('data', function(chunk) {
console.log(chunk.toString())
});
cb(false)
}
});
request.on('error', function(e) {
console.log("Error connecting to OneSignal")
console.log(e);
cb(false);
});
request.write(JSON.stringify(data))
request.end();
}
/**g
* Classify the device token of installations based on its device type.
* @param {Object} installations An array of installations
* @param {Array} validPushTypes An array of valid push types(string)
* @returns {Object} A map whose key is device type and value is an array of device
*/
function classifyInstallation(installations, validPushTypes) {
// Init deviceTokenMap, create a empty array for each valid pushType
let deviceMap = {};
for (let validPushType of validPushTypes) {
deviceMap[validPushType] = [];
}
for (let installation of installations) {
// No deviceToken, ignore
if (!installation.deviceToken) {
continue;
}
let pushType = installation.deviceType;
if (deviceMap[pushType]) {
deviceMap[pushType].push({
deviceToken: installation.deviceToken
});
} else {
console.log('Unknown push type from installation %j', installation);
}
}
return deviceMap;
}
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
OneSignalPushAdapter.classifyInstallation = classifyInstallation;
}
export default OneSignalPushAdapter;
module.exports = OneSignalPushAdapter;

View File

@@ -6,83 +6,46 @@
const Parse = require('parse/node').Parse;
const GCM = require('../../GCM');
const APNS = require('../../APNS');
import PushAdapter from './PushAdapter';
function ParsePushAdapter(pushConfig) {
this.validPushTypes = ['ios', 'android'];
this.senderMap = {};
pushConfig = pushConfig || {};
let pushTypes = Object.keys(pushConfig);
for (let pushType of pushTypes) {
if (this.validPushTypes.indexOf(pushType) < 0) {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'Push to ' + pushTypes + ' is not supported');
export class ParsePushAdapter extends PushAdapter {
constructor(pushConfig = {}) {
super(pushConfig);
this.validPushTypes = ['ios', 'android'];
this.senderMap = {};
let pushTypes = Object.keys(pushConfig);
for (let pushType of pushTypes) {
if (this.validPushTypes.indexOf(pushType) < 0) {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'Push to ' + pushTypes + ' is not supported');
}
switch (pushType) {
case 'ios':
this.senderMap[pushType] = new APNS(pushConfig[pushType]);
break;
case 'android':
this.senderMap[pushType] = new GCM(pushConfig[pushType]);
break;
}
}
switch (pushType) {
case 'ios':
this.senderMap[pushType] = new APNS(pushConfig[pushType]);
break;
case 'android':
this.senderMap[pushType] = new GCM(pushConfig[pushType]);
break;
}
send(data, installations) {
let deviceMap = PushAdapter.classifyInstallation(installations, this.validPushTypes);
let sendPromises = [];
for (let pushType in deviceMap) {
let sender = this.senderMap[pushType];
if (!sender) {
console.log('Can not find sender for push type %s, %j', pushType, data);
continue;
}
let devices = deviceMap[pushType];
sendPromises.push(sender.send(data, devices));
}
return Parse.Promise.when(sendPromises);
}
}
/**
* Get an array of valid push types.
* @returns {Array} An array of valid push types
*/
ParsePushAdapter.prototype.getValidPushTypes = function() {
return this.validPushTypes;
}
ParsePushAdapter.prototype.send = function(data, installations) {
let deviceMap = classifyInstallation(installations, this.validPushTypes);
let sendPromises = [];
for (let pushType in deviceMap) {
let sender = this.senderMap[pushType];
if (!sender) {
console.log('Can not find sender for push type %s, %j', pushType, data);
continue;
}
let devices = deviceMap[pushType];
sendPromises.push(sender.send(data, devices));
}
return Parse.Promise.when(sendPromises);
}
/**g
* Classify the device token of installations based on its device type.
* @param {Object} installations An array of installations
* @param {Array} validPushTypes An array of valid push types(string)
* @returns {Object} A map whose key is device type and value is an array of device
*/
function classifyInstallation(installations, validPushTypes) {
// Init deviceTokenMap, create a empty array for each valid pushType
let deviceMap = {};
for (let validPushType of validPushTypes) {
deviceMap[validPushType] = [];
}
for (let installation of installations) {
// No deviceToken, ignore
if (!installation.deviceToken) {
continue;
}
let pushType = installation.deviceType;
if (deviceMap[pushType]) {
deviceMap[pushType].push({
deviceToken: installation.deviceToken,
appIdentifier: installation.appIdentifier
});
} else {
console.log('Unknown push type from installation %j', installation);
}
}
return deviceMap;
}
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
ParsePushAdapter.classifyInstallation = classifyInstallation;
}
export default ParsePushAdapter;
module.exports = ParsePushAdapter;

View File

@@ -8,10 +8,47 @@
//
// Default is ParsePushAdapter, which uses GCM for
// android push and APNS for ios push.
export class PushAdapter {
send(devices, installations) { }
getValidPushTypes() { }
/**
* Get an array of valid push types.
* @returns {Array} An array of valid push types
*/
getValidPushTypes() {
return this.validPushTypes;
}
/**g
* Classify the device token of installations based on its device type.
* @param {Object} installations An array of installations
* @param {Array} validPushTypes An array of valid push types(string)
* @returns {Object} A map whose key is device type and value is an array of device
*/
static classifyInstallation(installations, validPushTypes) {
// Init deviceTokenMap, create a empty array for each valid pushType
let deviceMap = {};
for (let validPushType of validPushTypes) {
deviceMap[validPushType] = [];
}
for (let installation of installations) {
// No deviceToken, ignore
if (!installation.deviceToken) {
continue;
}
let pushType = installation.deviceType;
if (deviceMap[pushType]) {
deviceMap[pushType].push({
deviceToken: installation.deviceToken,
appIdentifier: installation.appIdentifier
});
} else {
console.log('Unknown push type from installation %j', installation);
}
}
return deviceMap;
}
}
export default PushAdapter;

View File

@@ -0,0 +1,63 @@
/*
AdaptableController.js
AdaptableController is the base class for all controllers
that support adapter,
The super class takes care of creating the right instance for the adapter
based on the parameters passed
*/
export class AdaptableController {
/**
* Check whether the api call has master key or not.
* @param {options} the adapter options
* @param {defaultAdapter} the default adapter class or object to use
* @discussion
* Supported options types:
* - string: the options will be loaded with required, when loaded, if default
* is set on the returned object, we'll use that one to support modules
* - object: a plain javascript object (options.constructor === Object), if options.adapter is set, we'll try to load it with the same mechanics
* - function: we'll create a new instance from that function, and pass the options object
*/
constructor(options, defaultAdapter) {
// Use the default by default
let adapter;
// We have options and options have adapter key
if (options) {
// Pass an adapter as a module name, a function or an instance
if (typeof options == "string" || typeof options == "function" || options.constructor != Object) {
adapter = options;
}
if (options.adapter) {
adapter = options.adapter;
}
}
if (!adapter) {
adapter = defaultAdapter;
}
// This is a string, require the module
if (typeof adapter === "string") {
adapter = require(adapter);
// If it's define as a module, get the default
if (adapter.default) {
adapter = adapter.default;
}
}
// From there it's either a function or an object
// if it's an function, instanciate and pass the options
if (typeof adapter === "function") {
var Adapter = adapter;
adapter = new Adapter(options);
}
this.adapter = adapter;
this.options = options;
}
}
export default AdaptableController;

View File

@@ -1,20 +1,18 @@
// FilesController.js
import { Parse } from 'parse/node';
import { randomHexString } from '../cryptoUtils';
import AdaptableController from './AdaptableController';
export class FilesController {
constructor(filesAdapter) {
this._filesAdapter = filesAdapter;
}
export class FilesController extends AdaptableController {
getFileData(config, filename) {
return this._filesAdapter.getFileData(config, filename);
return this.adapter.getFileData(config, filename);
}
createFile(config, filename, data) {
filename = randomHexString(32) + '_' + filename;
var location = this._filesAdapter.getFileLocation(config, filename);
return this._filesAdapter.createFile(config, filename, data).then(() => {
var location = this.adapter.getFileLocation(config, filename);
return this.adapter.createFile(config, filename, data).then(() => {
return Promise.resolve({
url: location,
name: filename
@@ -23,7 +21,7 @@ export class FilesController {
}
deleteFile(config, filename) {
return this._filesAdapter.deleteFile(config, filename);
return this.adapter.deleteFile(config, filename);
}
/**
@@ -49,7 +47,7 @@ export class FilesController {
if (filename.indexOf('tfss-') === 0) {
fileObject['url'] = 'http://files.parsetfss.com/' + config.fileKey + '/' + encodeURIComponent(filename);
} else {
fileObject['url'] = this._filesAdapter.getFileLocation(config, filename);
fileObject['url'] = this.adapter.getFileLocation(config, filename);
}
}
}

View File

@@ -1,5 +1,6 @@
import { Parse } from 'parse/node';
import PromiseRouter from '../PromiseRouter';
import AdaptableController from './AdaptableController';
const Promise = Parse.Promise;
const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000;
@@ -14,11 +15,7 @@ export const LogOrder = {
ASCENDING: 'asc'
}
export class LoggerController {
constructor(loggerAdapter, loggerOptions) {
this._loggerAdapter = loggerAdapter;
}
export class LoggerController extends AdaptableController {
// check that date input is valid
static validDateTime(date) {
@@ -59,7 +56,7 @@ export class LoggerController {
// order (optional) Direction of results returned, either “asc” or “desc”. Defaults to “desc”.
// size (optional) Number of rows returned by search. Defaults to 10
getLogs(options= {}) {
if (!this._loggerAdapter) {
if (!this.adapter) {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'Logger adapter is not availabe');
}
@@ -68,7 +65,7 @@ export class LoggerController {
options = LoggerController.parseOptions(options);
this._loggerAdapter.query(options, (result) => {
this.adapter.query(options, (result) => {
promise.resolve(result);
});
return promise;

View File

@@ -1,12 +1,9 @@
import { Parse } from 'parse/node';
import PromiseRouter from '../PromiseRouter';
import rest from '../rest';
import AdaptableController from './AdaptableController';
export class PushController {
constructor(pushAdapter) {
this._pushAdapter = pushAdapter;
};
export class PushController extends AdaptableController {
/**
* Check whether the deviceType parameter in qury condition is valid or not.
@@ -42,13 +39,12 @@ export class PushController {
}
sendPush(body = {}, where = {}, config, auth) {
var pushAdapter = this._pushAdapter;
var pushAdapter = this.adapter;
if (!pushAdapter) {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'Push adapter is not available');
}
PushController.validateMasterKey(auth);
PushController.validatePushType(where, pushAdapter.getValidPushTypes());
// Replace the expiration_time with a valid Unix epoch milliseconds time
body['expiration_time'] = PushController.getExpirationTime(body);

View File

@@ -67,9 +67,9 @@ function ParseServer({
appId,
masterKey,
databaseAdapter,
filesAdapter = new GridStoreAdapter(),
filesAdapter,
push,
loggerAdapter = new FileLoggerAdapter(),
loggerAdapter,
databaseURI,
cloud,
collectionPrefix = '',
@@ -91,15 +91,6 @@ function ParseServer({
DatabaseAdapter.setAdapter(databaseAdapter);
}
// Make push adapter
let pushConfig = push;
let pushAdapter;
if (pushConfig && pushConfig.adapter) {
pushAdapter = pushConfig.adapter;
} else if (pushConfig) {
pushAdapter = new ParsePushAdapter(pushConfig)
}
if (databaseURI) {
DatabaseAdapter.setAppDatabaseURI(appId, databaseURI);
}
@@ -114,9 +105,11 @@ function ParseServer({
}
}
const filesController = new FilesController(filesAdapter);
const pushController = new PushController(pushAdapter);
const loggerController = new LoggerController(loggerAdapter);
// We pass the options and the base class for the adatper,
// Note that passing an instance would work too
const filesController = new FilesController(filesAdapter, GridStoreAdapter);
const pushController = new PushController(push, new ParsePushAdapter(push));
const loggerController = new LoggerController(loggerAdapter, FileLoggerAdapter);
cache.apps[appId] = {
masterKey: masterKey,