From d50468158941a1f822b5b5b1a42db905b5c3d1f0 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Sun, 21 Feb 2016 12:02:18 -0500 Subject: [PATCH 1/4] 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 --- spec/AdaptableController.spec.js | 68 ++++ spec/MockAdapter.js | 3 + spec/OneSignalPushAdapter.spec.js | 3 +- src/Adapters/Push/OneSignalPushAdapter.js | 379 ++++++++++------------ src/Adapters/Push/ParsePushAdapter.js | 107 ++---- src/Adapters/Push/PushAdapter.js | 39 ++- src/Controllers/AdaptableController.js | 63 ++++ src/Controllers/FilesController.js | 16 +- src/Controllers/LoggerController.js | 11 +- src/Controllers/PushController.js | 10 +- src/index.js | 21 +- 11 files changed, 402 insertions(+), 318 deletions(-) create mode 100644 spec/AdaptableController.spec.js create mode 100644 spec/MockAdapter.js create mode 100644 src/Controllers/AdaptableController.js diff --git a/spec/AdaptableController.spec.js b/spec/AdaptableController.spec.js new file mode 100644 index 0000000000..12e7311317 --- /dev/null +++ b/spec/AdaptableController.spec.js @@ -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(); + }); +}); \ No newline at end of file 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/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..41cf81fd23 --- /dev/null +++ b/src/Controllers/AdaptableController.js @@ -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; \ No newline at end of file diff --git a/src/Controllers/FilesController.js b/src/Controllers/FilesController.js index e7d01763ec..285612cece 100644 --- a/src/Controllers/FilesController.js +++ b/src/Controllers/FilesController.js @@ -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); } } } diff --git a/src/Controllers/LoggerController.js b/src/Controllers/LoggerController.js index fe89446c73..f7f39a7865 100644 --- a/src/Controllers/LoggerController.js +++ b/src/Controllers/LoggerController.js @@ -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; diff --git a/src/Controllers/PushController.js b/src/Controllers/PushController.js index 3b73f16b91..feba11e9a0 100644 --- a/src/Controllers/PushController.js +++ b/src/Controllers/PushController.js @@ -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); diff --git a/src/index.js b/src/index.js index 36f9428409..b402732108 100644 --- a/src/index.js +++ b/src/index.js @@ -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, From 33fa5a7b2a2191dc9bdc4080bd9c21df3e408081 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Sun, 21 Feb 2016 16:54:30 -0500 Subject: [PATCH 2/4] Adds setDefaultAdapter --- spec/AdaptableController.spec.js | 6 ++++-- spec/LoggerController.spec.js | 3 ++- src/Controllers/AdaptableController.js | 20 +++++++++++++++----- src/index.js | 11 ++++++++--- 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/spec/AdaptableController.spec.js b/spec/AdaptableController.spec.js index 12e7311317..ce0ff20a20 100644 --- a/spec/AdaptableController.spec.js +++ b/spec/AdaptableController.spec.js @@ -47,14 +47,16 @@ describe("AdaptableController", ()=>{ }); it("should instantiate the default adapter from Class", (done) => { - var controller = new AdaptableController(null, FilesAdapter); + AdaptableController.setDefaultAdapter(FilesAdapter); + var controller = new AdaptableController(); 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); + AdaptableController.setDefaultAdapter(adapter); + var controller = new AdaptableController(); expect(controller.adapter).toBe(adapter); done(); }); diff --git a/spec/LoggerController.spec.js b/spec/LoggerController.spec.js index 3475495e4f..28c4bfbb65 100644 --- a/spec/LoggerController.spec.js +++ b/spec/LoggerController.spec.js @@ -76,12 +76,13 @@ describe('LoggerController', () => { }); it('should throw without an adapter', (done) => { - + LoggerController.setDefaultAdapter(undefined); var loggerController = new LoggerController(); expect(() => { loggerController.getLogs(); }).toThrow(); + LoggerController.setDefaultAdapter(FileLoggerAdapter); done(); }); }); diff --git a/src/Controllers/AdaptableController.js b/src/Controllers/AdaptableController.js index 41cf81fd23..552253e06c 100644 --- a/src/Controllers/AdaptableController.js +++ b/src/Controllers/AdaptableController.js @@ -8,6 +8,8 @@ based on the parameters passed */ +const DefaultAdapters = {}; + export class AdaptableController { /** * Check whether the api call has master key or not. @@ -17,12 +19,11 @@ export class AdaptableController { * 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 + * - 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 + constructor(options) { + let adapter; // We have options and options have adapter key @@ -37,7 +38,7 @@ export class AdaptableController { } if (!adapter) { - adapter = defaultAdapter; + adapter = this.defaultAdapter(); } // This is a string, require the module @@ -58,6 +59,15 @@ export class AdaptableController { this.adapter = adapter; this.options = options; } + + defaultAdapter() { + return DefaultAdapters[this.constructor.name]; + } + + // Sets the default adapter for that Class + static setDefaultAdapter(defaultAdapter) { + DefaultAdapters[this.name] = defaultAdapter; + } } export default AdaptableController; \ No newline at end of file diff --git a/src/index.js b/src/index.js index b402732108..e34f4c4abb 100644 --- a/src/index.js +++ b/src/index.js @@ -36,6 +36,11 @@ import { LogsRouter } from './Routers/LogsRouter'; import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter'; import { LoggerController } from './Controllers/LoggerController'; + +FilesController.setDefaultAdapter(GridStoreAdapter); +PushController.setDefaultAdapter(ParsePushAdapter); +LoggerController.setDefaultAdapter(FileLoggerAdapter); + // Mutate the Parse object to add the Cloud Code handlers addParseCloud(); @@ -107,9 +112,9 @@ function ParseServer({ // 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); + const filesController = new FilesController(filesAdapter); + const pushController = new PushController(push); + const loggerController = new LoggerController(loggerAdapter); cache.apps[appId] = { masterKey: masterKey, From 23e55e941e9f5f7809c082fc3054a74fc0d12e09 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Sun, 21 Feb 2016 23:47:07 -0500 Subject: [PATCH 3/4] Splits Adapter loading from AdaptableController - Adds dynamic prototype conformance check upon setting adapter - Throws when adapter is undefined, invalid in controller --- spec/AdaptableController.spec.js | 78 +++++++++----------------- spec/AdapterLoader.spec.js | 68 ++++++++++++++++++++++ spec/FilesController.spec.js | 4 +- spec/LoggerController.spec.js | 6 +- src/Adapters/AdapterLoader.js | 39 +++++++++++++ src/Controllers/AdaptableController.js | 78 ++++++++++++-------------- src/Controllers/FilesController.js | 7 ++- src/Controllers/LoggerController.js | 5 ++ src/Controllers/PushController.js | 12 +++- src/index.js | 17 +++--- 10 files changed, 204 insertions(+), 110 deletions(-) create mode 100644 spec/AdapterLoader.spec.js create mode 100644 src/Adapters/AdapterLoader.js diff --git a/spec/AdaptableController.spec.js b/spec/AdaptableController.spec.js index ce0ff20a20..70a80dfa7b 100644 --- a/spec/AdaptableController.spec.js +++ b/spec/AdaptableController.spec.js @@ -1,70 +1,44 @@ 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 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); + it("should use the provided adapter", (done) => { + var adapter = new FilesAdapter(); + var controller = new FilesController(adapter); + expect(controller.adapter).toBe(adapter); done(); }); - it("should instantiate the default adapter from Class", (done) => { - AdaptableController.setDefaultAdapter(FilesAdapter); - var controller = new AdaptableController(); - expect(controller.adapter instanceof FilesAdapter).toBe(true); + it("should throw when creating a new mock controller", (done) => { + var adapter = new FilesAdapter(); + expect(() => { + new MockController(adapter); + }).toThrow(); done(); }); - it("should use the default adapter", (done) => { - var adapter = new FilesAdapter(); - AdaptableController.setDefaultAdapter(adapter); - var controller = new AdaptableController(); - expect(controller.adapter).toBe(adapter); + 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 use the provided adapter", (done) => { - var adapter = new FilesAdapter(); - var controller = new AdaptableController(adapter); - expect(controller.adapter).toBe(adapter); + it("should fail to instantiate a controller without an adapter", (done) => { + expect(() => { + new FilesController(); + }).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 28c4bfbb65..9372ed9d18 100644 --- a/spec/LoggerController.spec.js +++ b/spec/LoggerController.spec.js @@ -76,13 +76,11 @@ describe('LoggerController', () => { }); it('should throw without an adapter', (done) => { - LoggerController.setDefaultAdapter(undefined); - var loggerController = new LoggerController(); + expect(() => { - loggerController.getLogs(); + var loggerController = new LoggerController(); }).toThrow(); - LoggerController.setDefaultAdapter(FileLoggerAdapter); done(); }); }); 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/Controllers/AdaptableController.js b/src/Controllers/AdaptableController.js index 552253e06c..fc014db14f 100644 --- a/src/Controllers/AdaptableController.js +++ b/src/Controllers/AdaptableController.js @@ -8,8 +8,6 @@ based on the parameters passed */ -const DefaultAdapters = {}; - export class AdaptableController { /** * Check whether the api call has master key or not. @@ -22,51 +20,49 @@ export class AdaptableController { * - 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) { - - 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 = this.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); - } - + constructor(adapter, options) { + this.setAdapter(adapter, options); + } + + setAdapter(adapter, options) { + this.validateAdapter(adapter); this.adapter = adapter; this.options = options; } - defaultAdapter() { - return DefaultAdapters[this.constructor.name]; + expectedAdapterType() { + throw new Error("Subclasses should implement expectedAdapterType()"); } - // Sets the default adapter for that Class - static setDefaultAdapter(defaultAdapter) { - DefaultAdapters[this.name] = defaultAdapter; + 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"); + } } } diff --git a/src/Controllers/FilesController.js b/src/Controllers/FilesController.js index 285612cece..fd5cec8da7 100644 --- a/src/Controllers/FilesController.js +++ b/src/Controllers/FilesController.js @@ -2,6 +2,7 @@ import { Parse } from 'parse/node'; import { randomHexString } from '../cryptoUtils'; import AdaptableController from './AdaptableController'; +import { FilesAdapter } from '../Adapters/Files/FilesAdapter'; export class FilesController extends AdaptableController { @@ -29,7 +30,7 @@ export class FilesController extends AdaptableController { * 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; @@ -52,6 +53,10 @@ export class FilesController extends AdaptableController { } } } + + expectedAdapterType() { + return FilesAdapter; + } } export default FilesController; diff --git a/src/Controllers/LoggerController.js b/src/Controllers/LoggerController.js index f7f39a7865..fb74aabd53 100644 --- a/src/Controllers/LoggerController.js +++ b/src/Controllers/LoggerController.js @@ -1,6 +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; @@ -70,6 +71,10 @@ export class LoggerController extends AdaptableController { }); return promise; } + + expectedAdapterType() { + return LoggerAdapter; + } } export default LoggerController; diff --git a/src/Controllers/PushController.js b/src/Controllers/PushController.js index feba11e9a0..22d9fe1135 100644 --- a/src/Controllers/PushController.js +++ b/src/Controllers/PushController.js @@ -2,6 +2,7 @@ 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 extends AdaptableController { @@ -25,7 +26,7 @@ export class PushController extends AdaptableController { deviceType + ' is not supported push type.'); } } - }; + } /** * Check whether the api call has master key or not. @@ -53,7 +54,8 @@ export class PushController extends AdaptableController { 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 @@ -80,7 +82,11 @@ export class PushController extends AdaptableController { 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 e34f4c4abb..92523ea8b5 100644 --- a/src/index.js +++ b/src/index.js @@ -33,14 +33,10 @@ 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'; - -FilesController.setDefaultAdapter(GridStoreAdapter); -PushController.setDefaultAdapter(ParsePushAdapter); -LoggerController.setDefaultAdapter(FileLoggerAdapter); - // Mutate the Parse object to add the Cloud Code handlers addParseCloud(); @@ -109,12 +105,17 @@ function ParseServer({ throw "argument 'cloud' must either be a string or a function"; } } + + + 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(filesAdapter); - const pushController = new PushController(push); - const loggerController = new LoggerController(loggerAdapter); + const filesController = new FilesController(filesControllerAdapter); + const pushController = new PushController(pushControllerAdapter); + const loggerController = new LoggerController(loggerControllerAdapter); cache.apps[appId] = { masterKey: masterKey, From 045caca9467ed6efa99848d84074600d64d4a017 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Mon, 22 Feb 2016 14:12:51 -0500 Subject: [PATCH 4/4] private _adapter, ES6 setters and getters --- spec/AdaptableController.spec.js | 43 ++++++++++++++++++++++++++ src/Controllers/AdaptableController.js | 30 ++++++++---------- 2 files changed, 56 insertions(+), 17 deletions(-) diff --git a/spec/AdaptableController.spec.js b/spec/AdaptableController.spec.js index 70a80dfa7b..3b275ec4cf 100644 --- a/spec/AdaptableController.spec.js +++ b/spec/AdaptableController.spec.js @@ -15,6 +15,11 @@ describe("AdaptableController", ()=>{ 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(); }); @@ -26,6 +31,17 @@ describe("AdaptableController", ()=>{ 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(); @@ -41,4 +57,31 @@ describe("AdaptableController", ()=>{ }).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/src/Controllers/AdaptableController.js b/src/Controllers/AdaptableController.js index fc014db14f..ef45b0225f 100644 --- a/src/Controllers/AdaptableController.js +++ b/src/Controllers/AdaptableController.js @@ -8,26 +8,22 @@ based on the parameters passed */ +// _adapter is private, use Symbol +var _adapter = Symbol(); + 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(adapter, options) { - this.setAdapter(adapter, options); + + constructor(adapter) { + this.adapter = adapter; } - - setAdapter(adapter, options) { + + set adapter(adapter) { this.validateAdapter(adapter); - this.adapter = adapter; - this.options = options; + this[_adapter] = adapter; + } + + get adapter() { + return this[_adapter]; } expectedAdapterType() {