├── .gitignore ├── LICENSE ├── README.md ├── __tests__ ├── apiTests.js ├── modelTests.js ├── optimizelyTest.js └── testUtils.js ├── lib ├── models │ ├── audience.js │ ├── experiment.js │ ├── goal.js │ ├── project.js │ └── variation.js ├── optimizely.js └── services │ ├── api.js │ ├── api_factory.js │ └── model_factory.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Optimizely 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Optimizely node.js bindings 2 | 3 | ## Installation 4 | 5 | `npm install optimizely-node` 6 | 7 | ## Documentation 8 | 9 | Documentation is available at http://developers.optimizely.com/rest/ 10 | 11 | ## API Overview 12 | 13 | Resources are accessible via the `optimizely` instance: 14 | 15 | ```js 16 | var optimizely = require('optimizely-node')(' your api key ') 17 | // optimizely.{ RESOURCE_NAME }.{ METHOD_NAME } 18 | ``` 19 | 20 | Every resource will return a promise, so you don't have to pass a callback. 21 | 22 | ```js 23 | optimizely.projects.fetch('').then(function(projectData) { 24 | //Do things with project data 25 | }, function(err) { 26 | //Handle errors 27 | }); 28 | ``` 29 | 30 | ## Available Objects & Methods 31 | 32 | 33 | * projects 34 | * `fetch(id)` 35 | * `fetchAll()` 36 | * `create(params)` 37 | * `save(instance)` 38 | * experiments 39 | * `fetch(id)` 40 | * `fetchAll()` 41 | * `create(params)` 42 | * `save(instance)` 43 | * `delete(instance)` 44 | * goals 45 | * `fetch(id)` 46 | * `fetchAll()` 47 | * `create(params)` 48 | * `save(instance)` 49 | * `delete(instance)` 50 | * audiences 51 | * `fetch(id)` 52 | * `fetchAll()` 53 | * `create(params)` 54 | * `save(instance)` 55 | * `delete(instance)` 56 | * variations 57 | * `fetch(id)` 58 | * `fetchAll()` 59 | * `create(params)` 60 | * `save(instance)` 61 | * `delete(instance)` 62 | 63 | -------------------------------------------------------------------------------- /__tests__/apiTests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // jest.dontMock('../lib/services/api_factory.js'); 3 | // jest.dontMock('request-promise'); 4 | // 5 | //TODO: AutoMock does not work with request-promise 6 | jest.autoMockOff(); 7 | 8 | describe('API Layer test suite', function() { 9 | it('tests API Object creation and config definition', function() { 10 | var Api = require('../lib/services/api_factory'); 11 | // var request = require('request-promise'); 12 | 13 | var api = Api.create({}); 14 | expect(api).toBeDefined(); 15 | expect(api._stack).toEqual([]); 16 | expect(api._filter).toEqual([]); 17 | 18 | var config = api._config; 19 | expect(config.headers).toEqual({}) 20 | expect(config.transforms.defaults).toBeDefined(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /__tests__/modelTests.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optimizely/optimizely-node/36a7a1f9163b5f6e302cee3268762d056cfb1f42/__tests__/modelTests.js -------------------------------------------------------------------------------- /__tests__/optimizelyTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | jest.autoMockOff(); 4 | 5 | describe('optimizely', function() { 6 | var utils = require('./testUtils'); 7 | 8 | it('creates an optimizley object with key', function() { 9 | var optly = require('../lib/optimizely')(utils.getUserOptimizelyKey()); 10 | expect(optly).toBeDefined(); 11 | expect(optly._api._config.headers['Token']).toBe(utils.getUserOptimizelyKey()); 12 | }); 13 | 14 | it('create Optimizely instance and test for model existence', function() { 15 | var optly = require('../lib/optimizely')(utils.getUserOptimizelyKey()); 16 | var resources = ['projects', 'goals', 'variations', 'audiences', 'experiments']; 17 | 18 | resources.forEach(function(resource) { 19 | expect(typeof optly[resource]).toBe('object'); 20 | expect(typeof optly[resource].instance).toBe('function'); 21 | }); 22 | }); 23 | 24 | it('getClientUserAgent returns legit UA', function() { 25 | var optly = require('../lib/optimizely')(utils.getUserOptimizelyKey()); 26 | var uaObj = JSON.parse(optly._api._config.headers['User-Agent']); 27 | expect(typeof uaObj).toBe('object'); 28 | expect(uaObj.lang).toBe('node'); 29 | expect(uaObj.publisher).toBe('optimizely'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /__tests__/testUtils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | getUserOptimizelyKey: function() { 5 | return process.env.OPTIMIZELY_TEST_API_KEY || 'abcdefjijklmnopqrstuv:123456'; 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /lib/models/audience.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Audience model 5 | * @author Jordan Garcia (jordan@optimizely.com) 6 | * @author Cheston Lee (cheston@optimizely.com) 7 | */ 8 | var modelFactory = require('../services/model_factory'); 9 | var _ = require('lodash'); 10 | 11 | module.exports = function(config) { 12 | return modelFactory.create(_.merge(_.clone(config), { 13 | entity: 'audiences', 14 | 15 | parent: { 16 | entity: 'projects', 17 | key: 'project_id' 18 | }, 19 | 20 | instance: function Audience() {}, 21 | 22 | editable: [ 23 | 'name', 24 | 'description', 25 | 'conditions', 26 | 'segmentation' 27 | ] 28 | })); 29 | }; 30 | -------------------------------------------------------------------------------- /lib/models/experiment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Experiment model 5 | * @author Jordan Garcia (jordan@optimizely.com) 6 | * @author Cheston Lee (cheston@optimizely.com) 7 | */ 8 | var modelFactory = require('../services/model_factory'); 9 | var _ = require('lodash'); 10 | 11 | module.exports = function(config) { 12 | return modelFactory.create(_.merge(_.clone(config), { 13 | entity: 'experiments', 14 | 15 | parent: { 16 | entity: 'projects', 17 | key: 'project_id' 18 | }, 19 | 20 | instance: function Experiment() {}, 21 | 22 | // TODO(jordan): fill out the field 23 | //fields: {}, 24 | })); 25 | }; 26 | -------------------------------------------------------------------------------- /lib/models/goal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Goal model 4 | * @author Jordan Garcia (jordan@optimizely.com) 5 | * @author Cheston Lee (cheston@optimizely.com) 6 | */ 7 | var modelFactory = require('../services/model_factory'); 8 | var _ = require('lodash'); 9 | 10 | module.exports = function(config) { 11 | return modelFactory.create(_.merge(_.clone(config), { 12 | entity: 'goals', 13 | 14 | parent: { 15 | entity: 'projects', 16 | key: 'project_id' 17 | }, 18 | 19 | instance: function Goal() {}, 20 | 21 | fields: { 22 | "metric": null, 23 | "is_editable": false, 24 | "target_to_experiments": null, 25 | "revenue_tracking_amount": null, 26 | "id": 860850647, 27 | "target_urls": [], 28 | "title": "Add to cart clicks", 29 | "preview_user_agent": "", 30 | "event": null, 31 | "url_match_types": [], 32 | "element_id": "", 33 | "project_id": 547944643, 34 | "goal_type": 0, 35 | "deleted": false, 36 | "experiment_ids": [], 37 | "selector": null, 38 | "multi_event": false, 39 | "created": "2014-04-20T18:20:10.991600Z", 40 | "target_url_match_types": [], 41 | "revenue_tracking": false, 42 | "preview_url": null, 43 | "addable": false, 44 | "urls": [] 45 | }, 46 | editable: [ 47 | 'addable', 48 | 'experiment_ids', 49 | 'goal_type', 50 | 'selector', 51 | 'target_to_experiments', 52 | 'target_urls', 53 | 'target_url_match_types', 54 | 'title', 55 | 'urls', 56 | 'url_match_types' 57 | ] 58 | })); 59 | }; 60 | -------------------------------------------------------------------------------- /lib/models/project.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Project model 4 | * @author Jordan Garcia (jordan@optimizely.com) 5 | * @author Cheston Lee (cheston@optimizely.com) 6 | */ 7 | var modelFactory = require('../services/model_factory'); 8 | var _ = require('lodash'); 9 | 10 | module.exports = function(config) { 11 | return modelFactory.create(_.merge(_.clone(config), { 12 | entity: 'projects', 13 | 14 | instance: function Project() {}, 15 | 16 | editable: [ 17 | 'ip_filter', 18 | 'include_jquery', 19 | 'project_name', 20 | 'project_status' 21 | ] 22 | })); 23 | }; 24 | -------------------------------------------------------------------------------- /lib/models/variation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Variation model 4 | * @author Jordan Garcia (jordan@optimizely.com) 5 | * @author Cheston Lee (cheston@optimizely.com) 6 | */ 7 | var modelFactory = require('../services/model_factory'); 8 | var _ = require('lodash'); 9 | 10 | module.exports = function(config) { 11 | return modelFactory.create(_.merge(_.clone(config), { 12 | entity: 'variations', 13 | 14 | parent: { 15 | entity: 'experiments', 16 | key: 'experiment_id' 17 | }, 18 | 19 | instance: function Variation() {}, 20 | 21 | editable: [ 22 | 'audience_ids', 23 | 'activation_mode', 24 | 'description', 25 | 'edit_url', 26 | 'status', 27 | 'custom_css', 28 | 'custom_js', 29 | 'percentage_included', 30 | 'url_conditions' 31 | ] 32 | })); 33 | }; 34 | -------------------------------------------------------------------------------- /lib/optimizely.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Node.js bindings for the Optimizely REST API 5 | * 6 | * @author Cheston Lee (cheston@optimizely.com) 7 | */ 8 | 9 | var api = require('./services/api_factory'); 10 | 11 | function Optimizely(key, version) { 12 | if(!(this instanceof Optimizely)) { 13 | return new Optimizely(key, version); 14 | } 15 | 16 | this._api = api.create({ 17 | protocol: Optimizely.DEFAULT_PROTOCOL, 18 | host: Optimizely.DEFAULT_HOST, 19 | port: Optimizely.DEFAULT_PORT, 20 | basePath: Optimizely.BASE_PATH, 21 | version: Optimizely.DEFAULT_API_VERSION, 22 | timeout: Optimizely.DEFAULT_TIMEOUT, 23 | headers: { 24 | 'Token': key, 25 | 'User-Agent': JSON.stringify(Optimizely.USER_AGENT) 26 | } 27 | }); 28 | 29 | this._prepResources(); 30 | } 31 | 32 | Optimizely.DEFAULT_HOST = 'www.optimizelyapis.com'; 33 | Optimizely.DEFAULT_PORT = '443'; 34 | Optimizely.BASE_PATH = '/experiment/v1'; 35 | Optimizely.DEFAULT_PROTOCOL = 'https://'; 36 | Optimizely.DEFAULT_API_VERSION = null; 37 | 38 | Optimizely.DEFAULT_TIMEOUT = require('http').createServer().timeout; 39 | Optimizely.PACKAGE_VERSION = require('../package.json').version; 40 | 41 | Optimizely.USER_AGENT = { 42 | bindings_version: Optimizely.PACKAGE_VERSION, 43 | lang: 'node', 44 | lang_version: process.version, 45 | publisher: 'optimizely' 46 | }; 47 | 48 | 49 | Optimizely.prototype = { 50 | _prepResources: function() { 51 | if (!this._api) throw Error('Optimizely Error: API resources not ready.'); 52 | 53 | var config = { api: this._api }; 54 | this.projects = require('./models/project')(config); 55 | this.experiments = require('./models/experiment')(config); 56 | this.variations = require('./models/variation')(config); 57 | this.goals = require('./models/goal')(config); 58 | this.audiences = require('./models/audience')(config); 59 | } 60 | }; 61 | 62 | module.exports = Optimizely; 63 | -------------------------------------------------------------------------------- /lib/services/api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Api service configured to work with the Experiment API 5 | * 6 | * Largely influenced by Restangular 7 | * 8 | * @author Jordan Garcia (jordan@optimizely.com) 9 | */ 10 | var apiFactory = require('./services/api_factory'); 11 | 12 | module.exports = function(apiConfig) { 13 | return apiFactory.create(apiConfig); 14 | }; 15 | -------------------------------------------------------------------------------- /lib/services/api_factory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Factory for generating API instances 4 | * 5 | * @author Cheston Lee (cheston@optimizely.com) 6 | * @author Jordan Garcia (jordan@optimizely.com) 7 | */ 8 | var request = require('request-promise'); 9 | var _ = require('lodash'); 10 | 11 | /** 12 | * Config Options 13 | * config.headers {Object} 14 | * config.baseUrl {String} 15 | * config.transforms {Object.<{ serialize: function, deserialize: function}>} 16 | * 17 | * @constructor 18 | * @param {Object} config 19 | */ 20 | function Api(config) { 21 | // config 22 | this._config = config || {}; 23 | this._config.transforms = _.merge(this._config.transforms || {},{ 24 | defaults: { 25 | serialize: JSON.stringify, 26 | deserialize: JSON.parse 27 | } 28 | }); 29 | 30 | this._config.headers = this._config.headers || {}; 31 | 32 | // stack of [('one' | 'all'), , ] 33 | // ex [['all', 'experiments'] or ['one', 'projects', 4001]] 34 | this._stack = []; 35 | 36 | /** 37 | * Array of filters to apply to URL 38 | * ex: /api/v1/projects/4001/experiments?filter=status:Started&filter=project_id:55 39 | * @var Array.<{field: string, value: string}> 40 | */ 41 | this._filter = []; 42 | } 43 | 44 | /** 45 | * Class level method to make an ajax request 46 | * Exists at class level for ease of testability 47 | * 48 | * Opts: 49 | * 'data' {Object} 50 | * 'method' {String} 'GET', 'PUT', 'POST', 'DELETE' 51 | * 'url' {String} 52 | * 53 | * @param {Object} opts 54 | * @param {Object=} headers 55 | * @return {Promise} 56 | */ 57 | Api.request = function(opts, headers) { 58 | if (!opts.method || !opts.url) { 59 | throw new Error("Must supply `opts.type` and `opts.url` to Api.request(opts)"); 60 | } 61 | 62 | var requestOpts = { 63 | method: opts.method, 64 | url: opts.url, 65 | encoding: 'utf8' 66 | }; 67 | 68 | if (!headers.Token) { 69 | throw new Error('Optimizely: Request requires API Token, found none.'); 70 | } 71 | 72 | if (opts.data) { 73 | headers['content-type'] = 'application/json'; 74 | requestOpts.body = opts.data; 75 | 76 | requestOpts.dataType = 'json'; 77 | } 78 | 79 | requestOpts.headers = headers; 80 | return request(requestOpts).promise(); 81 | }; 82 | 83 | /** 84 | * Appends '/{noun}/{id}' to the endpoint 85 | * @param {string} noun 86 | * @param {number} id 87 | * @return {Api} 88 | */ 89 | Api.prototype.one = function(noun, id) { 90 | this._stack.push(['one', noun, id]); 91 | return this; 92 | }; 93 | 94 | /** 95 | * Appends '/{noun}' to the endpoint 96 | * @param {string} noun 97 | * @return {Api} 98 | */ 99 | Api.prototype.all = function(noun) { 100 | this._stack.push(['all', noun]); 101 | return this; 102 | }; 103 | 104 | /** 105 | * Adds property to filter 106 | * 107 | * @param {String|Object} keyOrObject single key (to associate with val) or object of key/value pairs 108 | * @param {String=} val Value to match against 109 | * @return {Api} 110 | */ 111 | Api.prototype.filter = function(keyOrObject, val) { 112 | if (this._getMode() !== 'all') { 113 | throw new Error("ApiService Error: .filter() must be called in 'all' mode"); 114 | } 115 | 116 | var filters = keyOrObject; 117 | if (typeof keyOrObject === 'string') { 118 | filters = {}; 119 | // use 'true' if no value is provided 120 | filters[keyOrObject] = val; 121 | } 122 | 123 | _.each(filters, function(val, key) { 124 | this._filter.push([key, val]); 125 | }.bind(this)); 126 | 127 | return this; 128 | }; 129 | 130 | /** 131 | * Serializes instance data based on configured transforms on the entity 132 | * 133 | * @param {String} entity ex 'audiences' 134 | * @param {Object} data 135 | * @return {Object} serialized data 136 | */ 137 | Api.prototype.serialize = function(entity, data) { 138 | data = _.cloneDeep(data); 139 | if (this._config.transforms[entity]) { 140 | return this._config.transforms[entity].serialize(data); 141 | } else { 142 | return this._config.transforms.defaults.serialize(data); 143 | } 144 | }; 145 | 146 | /** 147 | * Serializes instance data based on configured transforms the entity 148 | * 149 | * @param {String} entity ex 'audiences' 150 | * @param {Object} data 151 | * @return {Object} deserialized data 152 | */ 153 | Api.prototype.deserialize = function(entity, data) { 154 | data = _.cloneDeep(data); 155 | if (this._config.transforms[entity]) { 156 | return this._config.transforms[entity].deserialize(data); 157 | } else { 158 | return this._config.transforms.defaults.deserialize(data); 159 | } 160 | return data; 161 | }; 162 | 163 | /** 164 | * Make POST request to current endpoint 165 | * @param {Object} data 166 | * @return {Promise} 167 | */ 168 | Api.prototype.post = function(data) { 169 | if (this._getMode() !== 'all') { 170 | throw new Error("ApiService Error: .post() must be called in 'all' mode"); 171 | } 172 | 173 | var entity = this._getEntity(); 174 | 175 | var opts = { 176 | method: 'POST', 177 | data: this.serialize(entity, data), 178 | url: this._getUrl() 179 | }; 180 | 181 | return Api.request(opts, this._config.headers).then(function(response) { 182 | this.reset(); 183 | return this.deserialize(entity, response); 184 | }.bind(this)); 185 | }; 186 | 187 | /** 188 | * Update with data 189 | * @param {Object} data 190 | * @return {Promise} 191 | */ 192 | Api.prototype.put = function(data) { 193 | if (this._getMode() !== 'one') { 194 | throw new Error("ApiService Error: .put() must be called in 'one' mode"); 195 | } 196 | 197 | var entity = this._getEntity(); 198 | 199 | var opts = { 200 | method: 'PUT', 201 | data: this.serialize(entity, data), 202 | url: this._getUrl() 203 | }; 204 | 205 | return Api.request(opts, this._config.headers).then(function(response) { 206 | this.reset(); 207 | return this.deserialize(entity, response); 208 | }.bind(this)); 209 | }; 210 | 211 | Api.prototype.reset = function() { 212 | this._stack = []; 213 | this._filters = []; 214 | }; 215 | 216 | /** 217 | * Performs a GET request to the current endpoint the isntance 218 | * is set to. 219 | * 220 | * @return {Promise} 221 | */ 222 | Api.prototype.get = function() { 223 | var opts = { 224 | method: 'GET', 225 | url: this._getUrl() 226 | }; 227 | 228 | // create a serialize function to pass to pipe 229 | var deserialize = this.deserialize.bind(this, this._getEntity()); 230 | // var mode = this._getMode(); 231 | var reset = this.reset.bind(this); 232 | 233 | return Api.request(opts, this._config.headers).then(function(results) { 234 | reset(); 235 | 236 | //TODO: Not sure why we make this differentiation 237 | // if (mode === 'one') { 238 | return deserialize(results); 239 | // } else if (mode === 'all') { 240 | // return results.map(deserialize); 241 | // } 242 | }, console.error); 243 | }; 244 | 245 | /** 246 | * Performs a DELETE request to the current endpoint 247 | * 248 | * @return {Promise} 249 | */ 250 | Api.prototype.delete = function() { 251 | // TODO(jordan): should .delete() be callable after .all() 252 | if (this._getMode() !== 'one') { 253 | throw new Error("Optimizely: .delete() must be called in 'one' mode"); 254 | } 255 | 256 | var opts = { 257 | method: 'DELETE', 258 | url: this._getUrl() 259 | }; 260 | 261 | return Api.request(opts, this._config.headers).then(this.reset); 262 | }; 263 | 264 | /** 265 | * Builds the url from this._stack 266 | * @private 267 | */ 268 | Api.prototype._getUrl = function() { 269 | var url = this._config.protocol + this._config.host + this._config.basePath || ''; 270 | var filters = []; 271 | 272 | url = this._stack.reduce(function(memo, item) { 273 | var mode = item[0]; // 'one' or 'all' 274 | memo += '/' + item[1]; // noun 275 | if (mode === 'one') { 276 | memo += '/' + item[2]; // id 277 | } 278 | return memo; 279 | }, url); 280 | 281 | if (this._filter.length > 0) { 282 | url += '?'; 283 | filters = this._filter.map(function(tuple) { 284 | return 'filter=' + tuple[0] + ':' + tuple[1]; 285 | }); 286 | url += filters.join('&'); 287 | } 288 | 289 | return url + '/'; 290 | }; 291 | 292 | /** 293 | * Gets the entity of the url 294 | * @private 295 | */ 296 | Api.prototype._getEntity = function() { 297 | return this._stack[this._stack.length - 1][1]; 298 | }; 299 | 300 | /** 301 | * Gets the mode of the request ('one' | 'all') 302 | * @private 303 | */ 304 | Api.prototype._getMode = function() { 305 | return this._stack[this._stack.length - 1][0]; 306 | }; 307 | 308 | module.exports = { 309 | // expose the Api constructor 310 | Api: Api, 311 | // the create function used to make an api instance 312 | /** 313 | * @param {{ 314 | * headers: object, 315 | * baseUrl: string, 316 | * transforms: { 317 | * : { 318 | * serialize: function, 319 | * deserialize: function 320 | * } 321 | * } 322 | * }} config 323 | */ 324 | create: function(config) { 325 | return new Api(config); 326 | } 327 | }; 328 | -------------------------------------------------------------------------------- /lib/services/model_factory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Factory for creating simple model objects with a restful interface 4 | * 5 | * @author Jordan Garcia (jordan@optimizely.com) 6 | */ 7 | var _ = require('lodash'); 8 | 9 | /** 10 | * config options: 11 | * 'entity' {String} name of entity in the API, ex: 'experiments' (unique) 12 | * 'instance' {Function} blank named function used as instance constructor, ex: function Audience() {} 13 | * 'parent' {{ entity: String, key: String }} the required parent association for fetches 14 | * 'fields' {Object} (optional) hash of default values of the entity when using Model.create() 15 | * 16 | * @param {Object} config for the model and extends the model object with any properties/methods 17 | * @return {Function} constructor 18 | */ 19 | function createModel(config) { 20 | if (!config.entity) { 21 | throw new Error('"entity" is required'); 22 | } 23 | 24 | var InstanceConstructor = config.instance || function ModelInstance() {}; 25 | var api = config.api; 26 | 27 | // Mixin methods that all models have 28 | var BaseModel = { 29 | // expose the instance constructor for `(instance instanceof Model.instance) checks 30 | instance: InstanceConstructor, 31 | 32 | /** 33 | * Creates a new object with the config.fields as the default values 34 | * 35 | * @param {Object=} data 36 | * @return {Object} 37 | */ 38 | create: function(data) { 39 | // use the supplied constructor 40 | // This allows a user to pass in instance: function Audience() {} 41 | // and have the Audience() function return an instance of Audience 42 | var instance = new InstanceConstructor(); 43 | var instanceData = _.extend( 44 | {}, 45 | _.cloneDeep(config.fields || {}), 46 | _.cloneDeep(data || {}) 47 | ); 48 | // populate the instanceor 49 | _.extend(instance, instanceData); 50 | return instance; 51 | }, 52 | 53 | /** 54 | * Persists entity using rest API 55 | * 56 | * @param {Model} instance 57 | * @return {Promise} 58 | */ 59 | save: function(instance) { 60 | var loadData = function(data) { 61 | return _.extend(instance, data); 62 | }; 63 | 64 | if (instance.id) { 65 | // do PUT save 66 | return api 67 | .one(config.entity, instance.id) 68 | .put(instance) 69 | .then(loadData, console.error); 70 | } else { 71 | // no id is set, do a POST 72 | var endpoint = api; 73 | if (config.parent) { 74 | endpoint.one(config.parent.entity, instance[config.parent.key]); 75 | } 76 | endpoint.all(config.entity); 77 | 78 | return endpoint 79 | .post(instance) 80 | .then(loadData, console.error); 81 | } 82 | }, 83 | 84 | /** 85 | * Fetch and return an entity 86 | * @param entityId Id of Entity to fetch 87 | * @returns {Deferred} Resolves to fetched Model instance 88 | */ 89 | fetch: function(entityId) { 90 | return api 91 | .one(config.entity, entityId) 92 | .get() 93 | .then(this.create, console.error); 94 | }, 95 | 96 | /** 97 | * Fetches all the entities that match the supplied filters 98 | * If the model has a parent association than the parent.key must be 99 | * supplied. 100 | * @param {Object|undefined} filters (optional) 101 | * @return {Deferred} 102 | */ 103 | fetchAll: function(filters) { 104 | filters = _.clone(filters || {}); 105 | var endpoint = api; 106 | 107 | if (config.parent && !filters[config.parent.key]) { 108 | throw new Error("fetchAll: must supply the parent.key as a filter to fetch all entities"); 109 | } 110 | 111 | if (config.parent) { 112 | endpoint.one(config.parent.entity, filters[config.parent.key]); 113 | // since the filtering is happening in the endpoint url we dont need filters 114 | delete filters[config.parent.key]; 115 | } 116 | 117 | return endpoint 118 | .all(config.entity) 119 | .filter(filters) 120 | .get() 121 | .then(function(results) { 122 | return results.map(this.create); 123 | // }.bind(this), console.error); 124 | }.bind(this), function(err) {console.log(err); throw new Error(err);}); 125 | }, 126 | 127 | /** 128 | * Makes an API request to delete the instance by id 129 | * @param {Model} instance 130 | */ 131 | delete: function(instance) { 132 | if (!instance.id) { 133 | throw new Error("delete(): `id` must be defined"); 134 | } 135 | 136 | return api 137 | .one(config.entity, instance.id) 138 | .delete(); 139 | }, 140 | 141 | /** 142 | * Checks if the passed in object is an instance of the supplied 143 | * def.instance function constructor 144 | * 145 | * @param {Object} instance 146 | * @param {Boolean} 147 | */ 148 | isInstance: function(instance) { 149 | return (instance instanceof this.instance); 150 | }, 151 | 152 | /** 153 | * Deserializes API data to be used in Javascript 154 | * @param {Object} data 155 | * @return {Object} 156 | */ 157 | deserialize: function(data) { 158 | return api.deserialize(config.entity, data); 159 | }, 160 | 161 | /** 162 | * Serializes the model instance data and prepares it in a format that is 163 | * consumable by the API 164 | * 165 | * @param {Object} data 166 | * @return {Object} 167 | */ 168 | serialize: function(data) { 169 | return api.serialize(config.entity, data); 170 | } 171 | }; 172 | 173 | return _.extend(BaseModel, config); 174 | } 175 | 176 | module.exports = { 177 | create: createModel 178 | }; 179 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "optimizely-node", 3 | "version": "0.0.3", 4 | "description": "Node.js wrapper for Optimizely REST API", 5 | "main": "lib/optimizely.js", 6 | "scripts": { 7 | "test": "jest" 8 | }, 9 | "keywords": [ 10 | "optimizely" 11 | ], 12 | "author": "Cheston Lee cheston@optimizely.com", 13 | "license": "ISC", 14 | "dependencies": { 15 | "es6-promise": "^1.0.0", 16 | "lodash": "^2.4.1", 17 | "request-promise": "^0.3.1" 18 | }, 19 | "devDependencies": { 20 | "jest-cli": "^0.1.18" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git://github.com/optimizely/optimizely-node.git" 25 | }, 26 | "bugs:": "https://github.com/optimizely/optimizely-node/issues" 27 | } 28 | --------------------------------------------------------------------------------