diff --git a/spec/AdaptableController.spec.js b/spec/AdaptableController.spec.js new file mode 100644 index 0000000000..3b275ec4cf --- /dev/null +++ b/spec/AdaptableController.spec.js @@ -0,0 +1,87 @@ + +var AdaptableController = require("../src/Controllers/AdaptableController").AdaptableController; +var FilesAdapter = require("../src/Adapters/Files/FilesAdapter").default; +var FilesController = require("../src/Controllers/FilesController").FilesController; + +var MockController = function(options) { + AdaptableController.call(this, options); +} +MockController.prototype = Object.create(AdaptableController.prototype); +MockController.prototype.constructor = AdaptableController; + +describe("AdaptableController", ()=>{ + + it("should use the provided adapter", (done) => { + var adapter = new FilesAdapter(); + var controller = new FilesController(adapter); + expect(controller.adapter).toBe(adapter); + // make sure _adapter is private + expect(controller._adapter).toBe(undefined); + // Override _adapter is not doing anything + controller._adapter = "Hello"; + expect(controller.adapter).toBe(adapter); + done(); + }); + + it("should throw when creating a new mock controller", (done) => { + var adapter = new FilesAdapter(); + expect(() => { + new MockController(adapter); + }).toThrow(); + done(); + }); + + it("should fail setting the wrong adapter to the controller", (done) => { + function WrongAdapter() {}; + var adapter = new FilesAdapter(); + var controller = new FilesController(adapter); + var otherAdapter = new WrongAdapter(); + expect(() => { + controller.adapter = otherAdapter; + }).toThrow(); + done(); + }); + + it("should fail to instantiate a controller with wrong adapter", (done) => { + function WrongAdapter() {}; + var adapter = new WrongAdapter(); + expect(() => { + new FilesController(adapter); + }).toThrow(); + done(); + }); + + it("should fail to instantiate a controller without an adapter", (done) => { + expect(() => { + new FilesController(); + }).toThrow(); + done(); + }); + + it("should accept an object adapter", (done) => { + var adapter = { + createFile: function(config, filename, data) { }, + deleteFile: function(config, filename) { }, + getFileData: function(config, filename) { }, + getFileLocation: function(config, filename) { }, + } + expect(() => { + new FilesController(adapter); + }).not.toThrow(); + done(); + }); + + it("should accept an object adapter", (done) => { + function AGoodAdapter() {}; + AGoodAdapter.prototype.createFile = function(config, filename, data) { }; + AGoodAdapter.prototype.deleteFile = function(config, filename) { }; + AGoodAdapter.prototype.getFileData = function(config, filename) { }; + AGoodAdapter.prototype.getFileLocation = function(config, filename) { }; + + var adapter = new AGoodAdapter(); + expect(() => { + new FilesController(adapter); + }).not.toThrow(); + done(); + }); +}); \ No newline at end of file diff --git a/spec/AdapterLoader.spec.js b/spec/AdapterLoader.spec.js new file mode 100644 index 0000000000..d577896934 --- /dev/null +++ b/spec/AdapterLoader.spec.js @@ -0,0 +1,68 @@ + +var AdapterLoader = require("../src/Adapters/AdapterLoader").AdapterLoader; +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 adapter = AdapterLoader.load({ + adapter: adapterPath, + key: "value", + foo: "bar" + }); + + expect(adapter instanceof Object).toBe(true); + expect(adapter.options.key).toBe("value"); + expect(adapter.options.foo).toBe("bar"); + done(); + }); + + it("should instantiate an adapter from string", (done) => { + var adapterPath = require('path').resolve("./spec/MockAdapter"); + var adapter = AdapterLoader.load(adapterPath); + + expect(adapter instanceof Object).toBe(true); + expect(adapter.options).toBe(adapterPath); + done(); + }); + + it("should instantiate an adapter from string that is module", (done) => { + var adapterPath = require('path').resolve("./src/Adapters/Files/FilesAdapter"); + var adapter = AdapterLoader.load({ + adapter: adapterPath + }); + + expect(adapter instanceof FilesAdapter).toBe(true); + done(); + }); + + it("should instantiate an adapter from function/Class", (done) => { + var adapter = AdapterLoader.load({ + adapter: FilesAdapter + }); + expect(adapter instanceof FilesAdapter).toBe(true); + done(); + }); + + it("should instantiate the default adapter from Class", (done) => { + var adapter = AdapterLoader.load(null, FilesAdapter); + expect(adapter instanceof FilesAdapter).toBe(true); + done(); + }); + + it("should use the default adapter", (done) => { + var defaultAdapter = new FilesAdapter(); + var adapter = AdapterLoader.load(null, defaultAdapter); + expect(adapter instanceof FilesAdapter).toBe(true); + done(); + }); + + it("should use the provided adapter", (done) => { + var originalAdapter = new FilesAdapter(); + var adapter = AdapterLoader.load(originalAdapter); + expect(adapter).toBe(originalAdapter); + done(); + }); +}); \ No newline at end of file diff --git a/spec/FilesController.spec.js b/spec/FilesController.spec.js index b0c6f56808..67b36de906 100644 --- a/spec/FilesController.spec.js +++ b/spec/FilesController.spec.js @@ -1,4 +1,5 @@ var FilesController = require('../src/Controllers/FilesController').FilesController; +var GridStoreAdapter = require("../src/Adapters/Files/GridStoreAdapter").GridStoreAdapter; var Config = require("../src/Config"); // Small additional tests to improve overall coverage @@ -6,7 +7,8 @@ describe("FilesController",()=>{ it("should properly expand objects", (done) => { var config = new Config(Parse.applicationId); - var filesController = new FilesController(); + var adapter = new GridStoreAdapter(); + var filesController = new FilesController(adapter); var result = filesController.expandFilesInObject(config, function(){}); expect(result).toBeUndefined(); diff --git a/spec/LoggerController.spec.js b/spec/LoggerController.spec.js index 3475495e4f..9372ed9d18 100644 --- a/spec/LoggerController.spec.js +++ b/spec/LoggerController.spec.js @@ -76,11 +76,10 @@ describe('LoggerController', () => { }); it('should throw without an adapter', (done) => { - - var loggerController = new LoggerController(); + expect(() => { - loggerController.getLogs(); + var loggerController = new LoggerController(); }).toThrow(); done(); }); diff --git a/spec/MockAdapter.js b/spec/MockAdapter.js new file mode 100644 index 0000000000..60d8ef8686 --- /dev/null +++ b/spec/MockAdapter.js @@ -0,0 +1,3 @@ +module.exports = function(options) { + this.options = options; +} diff --git a/spec/OneSignalPushAdapter.spec.js b/spec/OneSignalPushAdapter.spec.js index e7f3176871..a49e5e8d81 100644 --- a/spec/OneSignalPushAdapter.spec.js +++ b/spec/OneSignalPushAdapter.spec.js @@ -227,7 +227,8 @@ describe('OneSignalPushAdapter', () => { function makeDevice(deviceToken, appIdentifier) { return { - deviceToken: deviceToken + deviceToken: deviceToken, + appIdentifier: appIdentifier }; } diff --git a/src/Adapters/AdapterLoader.js b/src/Adapters/AdapterLoader.js new file mode 100644 index 0000000000..a0e0b877c6 --- /dev/null +++ b/src/Adapters/AdapterLoader.js @@ -0,0 +1,39 @@ + +export class AdapterLoader { + static load(options, defaultAdapter) { + 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); + } + return adapter; + } +} + +export default AdapterLoader; diff --git a/src/Adapters/Push/OneSignalPushAdapter.js b/src/Adapters/Push/OneSignalPushAdapter.js index 59a660f9ee..2f832f82d6 100644 --- a/src/Adapters/Push/OneSignalPushAdapter.js +++ b/src/Adapters/Push/OneSignalPushAdapter.js @@ -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']; + + send(data, installations) { + console.log("Sending notification to "+installations.length+" devices.") + 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]; + + if(devices.length > 0) { + sendPromises.push(sender(data, devices)); + } } - 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']; + return Parse.Promise.when(sendPromises); } - 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']; - } - - 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"); + sendToAPNS(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(offset >= tokenlength) { - promise.resolve() - } else { - this.sendNext(); + if(data['alert']) { + post['contents'] = {en: data['alert']}; + delete data['alert']; } - }.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; -} - - -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) + if(data['sound']) { + post['ios_sound'] = data['sound']; + delete data['sound']; } - }); - 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] = []; + 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; } - for (let installation of installations) { - // No deviceToken, ignore - if (!installation.deviceToken) { - continue; + + sendToGCM(data,tokens) { + data= deepcopy(data['data']); + + var post = {}; + + if(data['alert']) { + post['contents'] = {en: data['alert']}; + delete data['alert']; } - let pushType = installation.deviceType; - if (deviceMap[pushType]) { - deviceMap[pushType].push({ - deviceToken: installation.deviceToken - }); - } else { - console.log('Unknown push type from installation %j', installation); + if(data['title']) { + post['title'] = {en: data['title']}; + delete data['title']; } + if(data['uri']) { + post['url'] = data['uri']; + } + + 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_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; + } + + 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(); } - return deviceMap; } -if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') { - OneSignalPushAdapter.classifyInstallation = classifyInstallation; -} + +export default OneSignalPushAdapter; module.exports = OneSignalPushAdapter; diff --git a/src/Adapters/Push/ParsePushAdapter.js b/src/Adapters/Push/ParsePushAdapter.js index 1ae1647f92..00b1e9b416 100644 --- a/src/Adapters/Push/ParsePushAdapter.js +++ b/src/Adapters/Push/ParsePushAdapter.js @@ -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'); - } - switch (pushType) { - case 'ios': - this.senderMap[pushType] = new APNS(pushConfig[pushType]); - break; - case 'android': - this.senderMap[pushType] = new GCM(pushConfig[pushType]); - break; +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; + } } } -} - -/** - * 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; + + 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)); } - let devices = deviceMap[pushType]; - sendPromises.push(sender.send(data, devices)); + return Parse.Promise.when(sendPromises); } - 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; diff --git a/src/Adapters/Push/PushAdapter.js b/src/Adapters/Push/PushAdapter.js index 1e07467fa0..e83ec62adb 100644 --- a/src/Adapters/Push/PushAdapter.js +++ b/src/Adapters/Push/PushAdapter.js @@ -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; diff --git a/src/Controllers/AdaptableController.js b/src/Controllers/AdaptableController.js new file mode 100644 index 0000000000..ef45b0225f --- /dev/null +++ b/src/Controllers/AdaptableController.js @@ -0,0 +1,65 @@ +/* +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 + + */ + +// _adapter is private, use Symbol +var _adapter = Symbol(); + +export class AdaptableController { + + constructor(adapter) { + this.adapter = adapter; + } + + set adapter(adapter) { + this.validateAdapter(adapter); + this[_adapter] = adapter; + } + + get adapter() { + return this[_adapter]; + } + + expectedAdapterType() { + throw new Error("Subclasses should implement expectedAdapterType()"); + } + + validateAdapter(adapter) { + + if (!adapter) { + throw new Error(this.constructor.name+" requires an adapter"); + } + + let Type = this.expectedAdapterType(); + // Allow skipping for testing + if (!Type) { + return; + } + + // Makes sure the prototype matches + let mismatches = Object.getOwnPropertyNames(Type.prototype).reduce( (obj, key) => { + const adapterType = typeof adapter[key]; + const expectedType = typeof Type.prototype[key]; + if (adapterType !== expectedType) { + obj[key] = { + expected: expectedType, + actual: adapterType + } + } + return obj; + }, {}); + + if (Object.keys(mismatches).length > 0) { + console.error(adapter, mismatches); + throw new Error("Adapter prototype don't match expected prototype"); + } + } +} + +export default AdaptableController; \ No newline at end of file diff --git a/src/Controllers/FilesController.js b/src/Controllers/FilesController.js index e7d01763ec..fd5cec8da7 100644 --- a/src/Controllers/FilesController.js +++ b/src/Controllers/FilesController.js @@ -1,20 +1,19 @@ // FilesController.js import { Parse } from 'parse/node'; import { randomHexString } from '../cryptoUtils'; +import AdaptableController from './AdaptableController'; +import { FilesAdapter } from '../Adapters/Files/FilesAdapter'; -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 +22,7 @@ export class FilesController { } deleteFile(config, filename) { - return this._filesAdapter.deleteFile(config, filename); + return this.adapter.deleteFile(config, filename); } /** @@ -31,7 +30,7 @@ export class FilesController { * with the current mount point and app id. * Object may be a single object or list of REST-format objects. */ - expandFilesInObject(config, object) { + expandFilesInObject(config, object) { if (object instanceof Array) { object.map((obj) => this.expandFilesInObject(config, obj)); return; @@ -49,11 +48,15 @@ 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); } } } } + + expectedAdapterType() { + return FilesAdapter; + } } export default FilesController; diff --git a/src/Controllers/LoggerController.js b/src/Controllers/LoggerController.js index fe89446c73..fb74aabd53 100644 --- a/src/Controllers/LoggerController.js +++ b/src/Controllers/LoggerController.js @@ -1,5 +1,7 @@ import { Parse } from 'parse/node'; import PromiseRouter from '../PromiseRouter'; +import AdaptableController from './AdaptableController'; +import { LoggerAdapter } from '../Adapters/Logger/LoggerAdapter'; const Promise = Parse.Promise; const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000; @@ -14,11 +16,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 +57,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,11 +66,15 @@ export class LoggerController { options = LoggerController.parseOptions(options); - this._loggerAdapter.query(options, (result) => { + this.adapter.query(options, (result) => { promise.resolve(result); }); return promise; } + + expectedAdapterType() { + return LoggerAdapter; + } } export default LoggerController; diff --git a/src/Controllers/PushController.js b/src/Controllers/PushController.js index 3b73f16b91..22d9fe1135 100644 --- a/src/Controllers/PushController.js +++ b/src/Controllers/PushController.js @@ -1,12 +1,10 @@ import { Parse } from 'parse/node'; import PromiseRouter from '../PromiseRouter'; import rest from '../rest'; +import AdaptableController from './AdaptableController'; +import { PushAdapter } from '../Adapters/Push/PushAdapter'; -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. @@ -28,7 +26,7 @@ export class PushController { deviceType + ' is not supported push type.'); } } - }; + } /** * Check whether the api call has master key or not. @@ -42,13 +40,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); @@ -57,7 +54,8 @@ export class PushController { rest.find(config, auth, '_Installation', where).then(function(response) { return pushAdapter.send(body, response.results); }); - }; + } + /** * Get expiration time from the request body. * @param {Object} request A request object @@ -84,7 +82,11 @@ export class PushController { body['expiration_time'] + ' is not valid time.'); } return expirationTime.valueOf(); - }; + } + + expectedAdapterType() { + return PushAdapter; + } }; export default PushController; diff --git a/src/index.js b/src/index.js index 36f9428409..92523ea8b5 100644 --- a/src/index.js +++ b/src/index.js @@ -33,6 +33,7 @@ import { PushRouter } from './Routers/PushRouter'; import { FilesRouter } from './Routers/FilesRouter'; import { LogsRouter } from './Routers/LogsRouter'; +import { AdapterLoader } from './Adapters/AdapterLoader'; import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter'; import { LoggerController } from './Controllers/LoggerController'; @@ -67,9 +68,9 @@ function ParseServer({ appId, masterKey, databaseAdapter, - filesAdapter = new GridStoreAdapter(), + filesAdapter, push, - loggerAdapter = new FileLoggerAdapter(), + loggerAdapter, databaseURI, cloud, collectionPrefix = '', @@ -91,15 +92,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); } @@ -113,10 +105,17 @@ function ParseServer({ throw "argument 'cloud' must either be a string or a function"; } } - - const filesController = new FilesController(filesAdapter); - const pushController = new PushController(pushAdapter); - const loggerController = new LoggerController(loggerAdapter); + + + const filesControllerAdapter = AdapterLoader.load(filesAdapter, GridStoreAdapter); + const pushControllerAdapter = AdapterLoader.load(push, ParsePushAdapter); + const loggerControllerAdapter = AdapterLoader.load(loggerAdapter, FileLoggerAdapter); + + // We pass the options and the base class for the adatper, + // Note that passing an instance would work too + const filesController = new FilesController(filesControllerAdapter); + const pushController = new PushController(pushControllerAdapter); + const loggerController = new LoggerController(loggerControllerAdapter); cache.apps[appId] = { masterKey: masterKey,