From 4b629cbca6113ad9e333ef49e64cb05ca0e09509 Mon Sep 17 00:00:00 2001 From: Dustin Larimer Date: Sun, 13 Apr 2014 13:17:57 -0700 Subject: [PATCH] Updated Query model to support future query types --- index.js | 9 +- lib/query.js | 125 +++++++++++----------------- lib/requests.js | 21 ++++- test/test.js | 215 +++++++++++++++++++++++------------------------- 4 files changed, 172 insertions(+), 198 deletions(-) diff --git a/index.js b/index.js index 09a3ec8..6d0969d 100644 --- a/index.js +++ b/index.js @@ -191,8 +191,8 @@ function KeenApi(config) { request.get(self.readKey, path, params, callback); } }; - - _.extend(this, KeenQuery.client); + + this.run = KeenQuery.client.run; } function configure(config) { @@ -224,7 +224,6 @@ function decryptScopedKey(apiKey, scopedKey) { module.exports = { configure: configure, encryptScopedKey: encryptScopedKey, - decryptScopedKey: decryptScopedKey + decryptScopedKey: decryptScopedKey, + Query: KeenQuery.Query }; - -_.extend(module.exports, KeenQuery.analyses); \ No newline at end of file diff --git a/lib/query.js b/lib/query.js index a2f4765..bd64096 100644 --- a/lib/query.js +++ b/lib/query.js @@ -1,7 +1,7 @@ var _ = require('underscore'); var KeenRequests = require('./requests'); -/*! +/*! * ----------------- * Keen IO Query JS * ----------------- @@ -10,134 +10,101 @@ var KeenRequests = require('./requests'); var Keen = {}; // ------------------------------ -// Keen.Query +// Keen.Request // ------------------------------ -Keen.Query = function(){ +Keen.Request = function(){ this.data = {}; this.configure.apply(this, arguments); } -Keen.Query.prototype.configure = function(client, analyses, callback){ +Keen.Request.prototype.configure = function(client, queries, callback){ this.client = client; - this.analyses = analyses; + this.queries = queries; this.callback = callback; - this.refresh(); + this.run(); return this; }; -Keen.Query.prototype.refresh = function(){ - var self = this, - completions = 0, +Keen.Request.prototype.run = function(){ + var self = this, + completions = 0, response = []; - + var handleResponse = function(err, res){ if (err && self.callback) { return self.callback(err, null); } response[arguments[2]] = res, completions++; - if (completions == self.analyses.length) { - self.data = (self.analyses.length == 1) ? response[0] : response; + if (completions == self.queries.length) { + self.data = (self.queries.length == 1) ? response[0] : response; if (self.callback) self.callback(null, self.data); } }; - - _.each(self.analyses, function(analysis, index){ + + _.each(self.queries, function(query, index){ var data, path = '/projects/' + self.client.projectId; var callbackSequencer = function(err, res){ handleResponse(err, res, index); }; - - if (analysis instanceof Keen.Analysis) { - path += analysis.path; - data = analysis.params || {}; - - } + + if (query instanceof Keen.Query) { + path += query.path; + data = query.params || {}; + } /* TODO: Test and deploy this - else if (_.isString(analysis)) { - path += '/saved_queries/' + analysis + '/result'; + else if (_.isString(query)) { + path += '/saved_queries/' + query + '/result'; data = { api_key: self.client.readKey }; - - }*/ + }*/ else { - throw new Error('Analysis #' + (index+1) +' is not valid'); - + throw new Error('Query #' + (index+1) +' is not valid'); + } - + KeenRequests.get.call(self.client, self.client.readKey, path, data, callbackSequencer); }); - - return self; -}; -// Export .query method -// ------------------------------ -module.exports.client = { - query: function(input, callback){ - if (!input) throw new Error('Queries require at least one analysis'); - var analyses = (_.isArray(input)) ? input : [input]; - return new Keen.Query(this, analyses, callback); - } + return self; }; // ------------------------------ -// Keen.Analysis +// Keen.Query // ------------------------------ -Keen.Analysis = function(){}; +Keen.Query = function(){ + this.configure.apply(this, arguments); +}; -Keen.Analysis.prototype.configure = function(resource, collection, params){ - if (!collection) throw new Error('Event Collection name is required'); - this.path = '/queries/' + resource + '?event_collection=' + collection; - this.event_collection = collection; +Keen.Query.prototype.configure = function(analysisType, params){ + //if (!collection) throw new Error('Event Collection name is required'); + this.path = '/queries/' + analysisType; this.params = params || {}; return this; }; -Keen.Analysis.prototype.get = function(attribute) { +Keen.Query.prototype.get = function(attribute) { return this.params[attribute] || null; }; -Keen.Analysis.prototype.set = function(attributes) { +Keen.Query.prototype.set = function(attributes) { for (var attribute in attributes) { this.params[attribute] = attributes[attribute]; } return this; }; -// Analysis Types -// ------------------------------ -var analysisTypes = [ - 'Count', - 'Count_Unique', - 'Sum', - 'Average', - 'Minimum', - 'Maximum', - 'Select_Unique', - 'Extraction', - 'Funnel' -]; - -// Build and export methods -// ------------------------------ -module.exports.analyses = {}; -_.each(analysisTypes, function(type){ - Keen.Analysis[type] = function(){ - var args = Array.prototype.slice.call(arguments); - this.configure.apply(this, [type.toLowerCase()].concat(args)); - }; - Keen.Analysis[type].prototype = new Keen.Analysis(); - module.exports.analyses[type.replace("_","")] = Keen.Analysis[type]; -}); - -// Funnels are special +// Export Methods // ------------------------------ -Keen.Analysis.Funnel.prototype.configure = function(resource, params) { - if (!params.steps) throw new Error('Funnel steps are required'); - this.path = '/queries/' + resource; - this.params = params || {}; - return this; -} +module.exports = { + client: { + run: function(query, callback){ + if (!query) throw new Error('At least one query is required'); + var queries = (_.isArray(query)) ? query : [query]; + return new Keen.Request(this, queries, callback); + } + }, + Query: Keen.Query +}; diff --git a/lib/requests.js b/lib/requests.js index 348fc36..ed5bd15 100644 --- a/lib/requests.js +++ b/lib/requests.js @@ -1,9 +1,8 @@ var rest = require('superagent'); var crypto = require('crypto'); -var qs = require('querystring'); // Handle logic of processing response, including error messages -// The error handling should be strengthened over time to be more +// The error handling should be strengthened over time to be more // meaningful and robust // --------------------------------------------------------------- @@ -20,12 +19,26 @@ function processResponse(err, res, callback) { return callback(null, res.body); } +function buildQueryString(params){ + var query = []; + for (var key in params) { + if (params[key]) { + var value = params[key]; + if (Object.prototype.toString.call(value) !== '[object String]') { + value = JSON.stringify(value); + } + value = encodeURIComponent(value); + query.push(key + '=' + value); + } + } + return "?" + query.join('&'); +}; + module.exports = { get: function(apiKey, path, data, callback) { rest - .get(this.baseUrl + this.apiVersion + path) + .get(this.baseUrl + this.apiVersion + path + buildQueryString(data)) .set('Authorization', apiKey) - .query(data || {}) .end(function(err, res) { processResponse(err, res, callback); }); diff --git a/test/test.js b/test/test.js index 23e835a..377c14a 100644 --- a/test/test.js +++ b/test/test.js @@ -2,6 +2,21 @@ var should = require("should"); var _ = require('underscore'); +var buildQueryString = function(params){ + var query = []; + for (var key in params) { + if (params[key]) { + var value = params[key]; + if (Object.prototype.toString.call(value) !== '[object String]') { + value = JSON.stringify(value); + } + value = encodeURIComponent(value); + query.push(key + '=' + value); + } + } + return "?" + query.join('&'); +}; + describe("keen", function() { var keen; @@ -233,10 +248,10 @@ describe("keen", function() { }); }); }); - - + + describe('Queries', function() { - + beforeEach(function() { nock.cleanAll(); Keen = require("../"); @@ -245,122 +260,112 @@ describe("keen", function() { readKey: readKey }); }); - - describe('.query method', function(){ - + + describe('.run method', function(){ + it('should be a method', function(){ - keen['query'].should.be.a.Function; + keen['run'].should.be.a.Function; }); - + it('should throw an error when passed an invalid object', function(){ (function(){ - keen.query(null); + keen.run(null); }).should.throwError(); (function(){ - keen.query({}); + keen.run({}); }).should.throwError(); (function(){ - keen.query(0); + keen.run(0); }).should.throwError(); - + // This should be removed when 'saved_query' support is validated (function(){ - keen.query('string'); + keen.run('string'); }).should.throwError(); }); - + }); - + describe('Analysis Types', function(){ - + var analyses = [ - 'Count', - 'Count_Unique', - 'Sum', - 'Average', - 'Minimum', - 'Maximum', - 'Select_Unique', - 'Extraction' + 'count', + 'count_unique', + 'sum', + 'average', + 'minimum', + 'maximum', + 'select_unique', + 'extraction' ]; - + _.each(analyses, function(type){ - var method = type.replace('_',''); - var analysis = new Keen[method]('eventCollection', { timeframe: 'this_7_days' }); - var query_path = "/3.0/projects/" + projectId; - query_path += "/queries/" + type.toLowerCase(); - query_path += "?event_collection=" + analysis.event_collection; - - it('should be an instance of Keen.' + method, function(){ - analysis.should.be.an.instanceOf(Keen[method]); + var analysis = new Keen.Query(type, { + event_collection: 'eventCollection', + timeframe: 'this_7_days' }); - - it('should have a correct event_collection property', function(){ - analysis.should.have.property('event_collection'); - analysis.event_collection.should.eql('eventCollection'); - }); - - it('should throw an error if event_collection is missing', function(){ - (function(){ - var flawed_analysis = new Keen[method](); - }).should.throwError(); + var query_path = "/3.0/projects/" + projectId + "/queries/" + type; + + it('should be an instance of Keen.Query', function(){ + analysis.should.be.an.instanceOf(Keen.Query); }); - + it('should have a correct path propery', function(){ analysis.should.have.property('path'); - analysis.path.should.eql('/queries/' + type.toLowerCase() + '?event_collection=eventCollection'); + analysis.path.should.eql('/queries/' + type); }); - + it('should have a params property with supplied parameters', function(){ analysis.should.have.property('params'); + analysis.params.should.have.property('event_collection', 'eventCollection'); analysis.params.should.have.property('timeframe', 'this_7_days'); }); - + it('should have a #get method that returns a requested parameter', function(){ analysis.get.should.be.a.Function; analysis.get('timeframe').should.eql('this_7_days'); }); - + it('should have a #set method that sets all supplied properties', function(){ analysis.set.should.be.a.Function; analysis.set({ group_by: 'property', target_property: 'referrer' }); analysis.params.should.have.property('group_by', 'property'); analysis.params.should.have.property('target_property', 'referrer'); }); - - describe('When handled by .query method', function(){ - + + describe('When handled by .run method', function(){ + beforeEach(function() { nock.cleanAll(); }); describe('Single analyses', function(){ - + it('should return a response when successful', function(done){ var mockResponse = { result: 1 }; mockGetRequest(query_path, 200, mockResponse); analysis.params = {}; - var test = keen.query(analysis, function(err, res){ + var test = keen.run(analysis, function(err, res){ (err === null).should.be.true; res.should.eql(mockResponse); done(); }); }); - + it('should return an error when unsuccessful', function(done){ var mockResponse = { error_code: 'ResourceNotFoundError', message: 'no foo' }; mockGetRequest(query_path, 500, mockResponse); analysis.params = {}; - var test = keen.query(analysis, function(err, res){ + var test = keen.run(analysis, function(err, res){ err.should.be.an.instanceOf(Error); err.should.have.property('code', mockResponse.error_code); done(); }); }); - + }); - - + + describe('Multiple analyses', function(){ it('should return a single response when successful', function(done){ @@ -369,7 +374,7 @@ describe("keen", function() { mockGetRequest(query_path, 200, mockResponse); mockGetRequest(query_path, 200, mockResponse); analysis.params = {}; - var test = keen.query([analysis, analysis, analysis], function(err, res){ + var test = keen.run([analysis, analysis, analysis], function(err, res){ (err === null).should.be.true; res.should.be.an.Array; res.should.have.length(3); @@ -377,106 +382,96 @@ describe("keen", function() { done(); }); }); - + it('should return a single error when unsuccessful', function(done){ var mockResponse = { error_code: 'ResourceNotFoundError', message: 'no foo' }; mockGetRequest(query_path, 500, mockResponse); analysis.params = {}; - var test = keen.query([analysis, analysis, analysis], function(err, res){ + var test = keen.run([analysis, analysis, analysis], function(err, res){ err.should.be.an.instanceOf(Error); err.should.have.property('code', mockResponse.error_code); done(); }); }); }); - - + + }); - + }); - + }); - - + + describe('Funnels', function(){ - - var funnel = new Keen.Funnel({ + + var funnel = new Keen.Query('funnel', { steps: [ { event_collection: "view_landing_page", actor_property: "user.id" }, { event_collection: "sign_up", actor_property: "user.id" } - ] + ], + timeframe: "this_21_days" }); - var funnel_path = "/3.0/projects/" + projectId + "/queries/funnel"; - funnel_path += "?steps[0][event_collection]=view_landing_page"; - funnel_path += "&steps[0][actor_property]=user.id"; - funnel_path += "&steps[1][event_collection]=sign_up"; - funnel_path += "&steps[1][actor_property]=user.id"; - funnel_path += "&timeframe=this_21_days"; - - it('should be an instance of Keen.Funnel', function(){ - funnel.should.be.an.instanceOf(Keen.Funnel); - }); - - it('should throw an error if steps are missing', function(){ - (function(){ - var flawed_funnel = new Keen.Funnel(); - }).should.throwError(); + var funnel_path = "/3.0/projects/" + projectId + "/queries/funnel" + buildQueryString(funnel.params); + + it('should be an instance of Keen.Query', function(){ + funnel.should.be.an.instanceOf(Keen.Query); }); - + it('should have a correct path propery', function(){ funnel.should.have.property('path'); funnel.path.should.eql('/queries/funnel'); }); - + it('should have a params property with supplied parameters', function(){ funnel.should.have.property('params'); funnel.params.should.have.property('steps'); funnel.params.steps.should.be.an.Array; }); - + it('should have a #set method that sets all supplied properties', function(){ funnel.set.should.be.a.Function; funnel.set({ timeframe: 'this_21_days' }); funnel.params.should.have.property('timeframe', 'this_21_days'); }); - + it('should have a #get method that returns a requested parameter', function(){ funnel.get.should.be.a.Function; funnel.get('steps').should.be.an.Array; funnel.get('timeframe').should.eql('this_21_days'); }); - - describe('When handled by .query method', function(){ - + + describe('When handled by .run method', function(){ + beforeEach(function() { nock.cleanAll(); }); describe('Single analyses', function(){ - + it('should return a response when successful', function(done){ var mockResponse = { result: 1 }; mockGetRequest(funnel_path, 200, mockResponse); - var test = keen.query(funnel, function(err, res){ + var test = keen.run(funnel, function(err, res){ (err === null).should.be.true; res.should.eql(mockResponse); done(); }); }); - + it('should return an error when unsuccessful', function(done){ var mockResponse = { error_code: 'ResourceNotFoundError', message: 'no foo' }; mockGetRequest(funnel_path, 500, mockResponse); - var test = keen.query(funnel, function(err, res){ + var test = keen.run(funnel, function(err, res){ err.should.be.an.instanceOf(Error); err.should.have.property('code', mockResponse.error_code); done(); }); }); - + }); - - + + describe('Multiple analyses', function(){ it('should return a single response when successful', function(done){ @@ -484,7 +479,7 @@ describe("keen", function() { mockGetRequest(funnel_path, 200, mockResponse); mockGetRequest(funnel_path, 200, mockResponse); mockGetRequest(funnel_path, 200, mockResponse); - var test = keen.query([funnel, funnel, funnel], function(err, res){ + var test = keen.run([funnel, funnel, funnel], function(err, res){ (err === null).should.be.true; res.should.be.an.Array; res.should.have.length(3); @@ -492,26 +487,26 @@ describe("keen", function() { done(); }); }); - + it('should return a single error when unsuccessful', function(done){ var mockResponse = { error_code: 'ResourceNotFoundError', message: 'no foo' }; mockGetRequest(funnel_path, 500, mockResponse); - var test = keen.query([funnel, funnel, funnel], function(err, res){ + var test = keen.run([funnel, funnel, funnel], function(err, res){ err.should.be.an.instanceOf(Error); err.should.have.property('code', mockResponse.error_code); done(); }); }); - + }); - - + + }); - - - + + + }); - + }); - + });