diff --git a/.gitignore b/.gitignore index 0ac3200..e452874 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ node_modules/ *.sublime* sketch test.html +.c9/ +*.log diff --git a/README.md b/README.md index 4ed830d..f4e0621 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,11 @@ A [lightblue](https://github.com/lightblue-platform) client written in Javascript. -Conceivably one day useful for: +Write... - Node.JS apps talking to a Lightblue REST service - Client side apps communicating with a server that forwards requests to a Lightblue REST service -At the moment this is really just a rough sketch of an idea and will change drastically. - -# Install +## Install `bower install lightblue.js --save` @@ -16,30 +14,61 @@ At the moment this is really just a rough sketch of an idea and will change dras `git clone https://github.com/alechenninger/lightblue.js.git` -# Usage - -It does still sort of work! It won't actually make any requests, but gives you an API for building the key components of the request: the HTTP method, the URL, and the request body. The idea is this information could then be used easily with either XMLHttpRequest, jQuery.ajax, Angular's $http service, or a Node.JS HTTP client. All it takes is a little glue code to tie the necessary components with one of the aforementioned common AJAX mechanisms. -Use browserify `require` or commonjs `define`, or just include dist/lightblue.min.js and use the namespace `lightblue`. +## Imports -## Imports: +### Vanilla.js ```javascript -// Plain old HTML - +// No module framework (use window.lightblue) + +``` -// NodeJS or Browserify -var lightblue = require("./lightblue.min.js"); +### Browserify (CommonJS) or RequireJS (AMD) -// CommonJS and RequireJS work too but I don't have an example +```js +// commonjs +var lightblue = require("lightblue"); + +// asynchronous module definition (amd) +require(["lightblue"], function(lightblue) { + ... +}); ``` -## Construct a find request: +Once you have a `lightblue` object, you can get a client: -```javascript +```js // Assumes /data and /metadata for data and metadata services respectively, // but you can override. var client = lightblue.getClient("http://my.lightblue.host.com/rest"); +``` + +### AngularJS +If angular is detected, a "lightblue" module will be registered with a +"lightblue" service as the client. + +```js +var app = angular.module("app", ["lightblue"]); + +app.config(["lightblueProvider", function(lightblueProvider) { + lightblueProvider.setHost("http://my.lightblue.com"); +}]); + +app.controller("ctrl", ["lightblue", function(lightblueClient) { + lightblueClient.data.find(...) + .then(...); +}]); +``` + +**At the moment you will also need to use the global "lightblue" namespace if +you want query builder API. So don't name your client variable `lightblue` +just yet. See +[issue #9](https://github.com/alechenninger/lightblue.js/issues/9).** + +## Construct a find request + +```javascript var field = lightblue.field; var find = client.data.find({ @@ -51,39 +80,6 @@ var find = client.data.find({ .and(field("age").greaterThan(4))), // No projection builder yet but it would be something like this: projection: include("*").recursively() -}); - -assertEquals("http://my.lightblue.host.com/rest/data/find/User/1.0.0", find.url); -assertEquals("post", find.method); -assertEquals({ - objectType: "User", - version: "1.0.0", - query: { - $or: [ - { - field: "username", - op: "$eq", - rvalue: "bob" - }, - { - $and: [ - { - field: "firstName", - op: "$eq", - rfield: "username" - }, - { - field: "age", - op: "$gt", - rvalue: 4 - } - ] - } - ] - }, - projection: { - field: "*", - recursive: true - } - }, find.body); +}) +.then(console.log); ``` diff --git a/lib/client.js b/lib/client.js new file mode 100644 index 0000000..98d269f --- /dev/null +++ b/lib/client.js @@ -0,0 +1,6 @@ +module.exports = LightblueClient; + +function LightblueClient(dataClient, metadataClient) { + this.data = dataClient; + this.metadata = metadataClient; +} \ No newline at end of file diff --git a/lib/clientutil.js b/lib/clientutil.js index 7c16577..c7ca054 100644 --- a/lib/clientutil.js +++ b/lib/clientutil.js @@ -22,8 +22,55 @@ exports.resolve = function() { .filter(notEmpty) .map(trimSlashes) .join("/"); -} +}; exports.isEmpty = function(s) { return typeof s === "undefined" || s === ""; -} \ No newline at end of file +}; + +function ifDefined(it, otherwise) { + return isDefined(it) ? it : otherwise; +} + +function isDefined(it) { + return typeof it !== "undefined"; +} + +function isObject(it) { + return typeof it === "object"; +} + +var assertArg = { + isInstance: function(arg, ctor, name) { + if (!(arg instanceof ctor)) { + throw new Error("Expected instanceof " + ctor + " but " + name + " was " + + isObject(arg) ? arg.constructor : "undefined"); + } + + return arg; + }, + isNotBlankString: function(arg, name) { + if (typeof arg !== "string") { + throw new Error("Expected a string but " + name + " was " + typeof arg); + } + + if (arg.trim().length === 0) { + throw new Error("Expected non-blank string but " + name + " was empty."); + } + + return arg; + }, + isTypeOf: function(arg, type, name) { + if (typeof arg !== type) { + throw new Error("Expected a typeof " + type + " but " + name + " was " + + typeof arg); + } + + return arg; + } +}; + +exports.assertArg = assertArg; +exports.ifDefined = ifDefined; +exports.isDefined = isDefined; +exports.isObject = isObject; \ No newline at end of file diff --git a/lib/data.js b/lib/data.js index 0057f4e..cea313c 100644 --- a/lib/data.js +++ b/lib/data.js @@ -1,38 +1,47 @@ -var resolve = require("./clientutil").resolve; -var RestRequest = require("./rest").RestRequest; +// TODO: Consider using url.resolve? +var resolve = require("./clientutil").resolve; +var http = require("./http"); + +var HttpRequest = http.HttpRequest; module.exports = LightblueDataClient; /** * Client for making requests against a Lightblue data endpoint. * @constructor - * @param {String} host - The URL where the rest data server is deployed. This + * @param {HttpClient} httpClient + * @param {String} host The URL where the rest data server is deployed. This * path will be the base for requests, like ${host}/find, so if the app is * deployed under a particular context (like /rest/data), this must be * included in the host. */ -function LightblueDataClient(host) { - this.host = host; +function LightblueDataClient(httpClient, host) { + this._httpClient = httpClient; + this._host = host; } +LightblueDataClient.prototype._execute = function(request) { + return this._httpClient.execute(request); +}; + LightblueDataClient.prototype.find = function(config) { - return new FindRequest(this.host, config); + return this._execute(new FindRequest(this._host, config)); }; LightblueDataClient.prototype.insert = function(config) { - return new InsertRequest(this.host, config); + return this._execute(new InsertRequest(this._host, config)); }; LightblueDataClient.prototype.update = function(config) { - return new UpdateRequest(this.host, config); + return this._execute(new UpdateRequest(this._host, config)); }; LightblueDataClient.prototype.save = function(config) { - return new SaveRequest(this.host, config); + return this._execute(new SaveRequest(this._host, config)); }; LightblueDataClient.prototype.delete = function(config) { - return new DeleteRequest(this.host, config); + return this._execute(new DeleteRequest(this._host, config)); }; /** @@ -40,6 +49,7 @@ LightblueDataClient.prototype.delete = function(config) { */ function FindRequest(host, config) { var url = resolve(host, "find", config.entity, config.version); + var body = { objectType: config.entity, version: config.version, @@ -57,10 +67,10 @@ function FindRequest(host, config) { body.range[1] = config.range.to || config.range[1]; } - RestRequest.call(this, "post", url, body); + HttpRequest.call(this, "post", url, body); } -FindRequest.prototype = Object.create(RestRequest.prototype); +FindRequest.prototype = Object.create(HttpRequest.prototype); FindRequest.prototype.constructor = FindRequest; /** @@ -68,6 +78,7 @@ FindRequest.prototype.constructor = FindRequest; */ function InsertRequest(host, config) { var url = resolve(host, "insert", config.entity, config.version); + var body = { objectType: config.entity, version: config.version, @@ -75,10 +86,10 @@ function InsertRequest(host, config) { projection: config.projection }; - RestRequest.call(this, "put", url, body); + HttpRequest.call(this, "put", url, body); } -InsertRequest.prototype = Object.create(RestRequest.prototype); +InsertRequest.prototype = Object.create(HttpRequest.prototype); InsertRequest.prototype.constructor = InsertRequest; /** @@ -86,18 +97,19 @@ InsertRequest.prototype.constructor = InsertRequest; */ function SaveRequest(host, config) { var url = resolve(host, "save", config.entity, config.version); + var body = { objectType: config.entity, version: config.version, data: config.data, upsert: config.upsert, projection: config.projection - } + }; - RestRequest.call(this, "post", url, body); + HttpRequest.call(this, "post", url, body); } -SaveRequest.prototype = Object.create(RestRequest.prototype); +SaveRequest.prototype = Object.create(HttpRequest.prototype); SaveRequest.prototype.constructor = SaveRequest; /** @@ -105,6 +117,7 @@ SaveRequest.prototype.constructor = SaveRequest; */ function UpdateRequest(host, config) { var url = resolve(host, "update", config.entity, config.version); + var body = { objectType: config.entity, version: config.version, @@ -113,10 +126,10 @@ function UpdateRequest(host, config) { projection: config.projection }; - RestRequest.call(this, "post", url, body); + HttpRequest.call(this, "post", url, body); } -UpdateRequest.prototype = Object.create(RestRequest.prototype); +UpdateRequest.prototype = Object.create(HttpRequest.prototype); UpdateRequest.prototype.constructor = UpdateRequest; /** @@ -124,14 +137,15 @@ UpdateRequest.prototype.constructor = UpdateRequest; */ function DeleteRequest(host, config) { var url = resolve(host, "delete", config.entity, config.version); + var body = { objectType: config.entity, version: config.version, query: config.query }; - RestRequest.call(this, "post", url, body); + HttpRequest.call(this, "post", url, body); } -DeleteRequest.prototype = Object.create(RestRequest.prototype); +DeleteRequest.prototype = Object.create(HttpRequest.prototype); DeleteRequest.prototype.constructor = DeleteRequest; \ No newline at end of file diff --git a/lib/http.js b/lib/http.js new file mode 100644 index 0000000..1e71b75 --- /dev/null +++ b/lib/http.js @@ -0,0 +1,25 @@ +var assertArg = require("./clientutil.js").assertArg; + +/** + * @constructor + * @param {String} method Http method, case insensitive. + * @param {Url} url + * @param {String=} body The request body. + */ +exports.HttpRequest = function(method, url, body) { + this.method = assertArg.isNotBlankString(method, "method"); + this.url = assertArg.isTypeOf(url, "string", "url"); + this.body = body; + + this.METHOD = this.method.toUpperCase(); +}; + +/** + * @interface HttpClient + */ + +/** + * @function + * @name HttpClient#execute + * @return {Promise} + */ \ No newline at end of file diff --git a/lib/lightblue.js b/lib/lightblue.js index 3dc6be5..b46bf83 100644 --- a/lib/lightblue.js +++ b/lib/lightblue.js @@ -1,6 +1,12 @@ -var DataClient = require("./data.js"); -var MetadataClient = require("./metadata.js"); -var query = require("./query.js"); +var DataClient = require("./data.js"); +var MetadataClient = require("./metadata.js"); +var LightblueClient = require("./client.js"); +var query = require("./query.js"); +var NodeHttpClient = require("./nodehttp.js"); + +// TODO: reorganize modules +// Install angular module if angular is present +require("./nglightblue.js"); var resolve = require("./clientutil.js").resolve; @@ -9,33 +15,45 @@ exports.getMetadataClient = getMetadataClient; exports.getClient = getClient; exports.field = query.field; -function LightblueClient(dataClient, metadataClient) { - this.data = dataClient; - this.metadata = metadataClient; -} - /** * Returns a LightblueDataClient. * @param {String} dataHost The full path for the base Lightblue data REST * context. * @return {LightblueDataClient} */ -function getDataClient(dataHost) { - return new DataClient(dataHost); +function getDataClient(dataHost, options) { + return new DataClient(getHttpClient(options), dataHost); } -function getMetadataClient(metadataHost) { - return new MetadataClient(metadataHost); +function getMetadataClient(metadataHost, options) { + return new MetadataClient(getHttpClient(options), metadataHost); } -function getClient(dataHost, metadataHost) { - if (typeof metadataHost === "undefined") { +/** + * Returns a LightblueClient. + * @param {String} dataHost The full path for the base Lightblue data service. + * @param {String=} metadataHost The full path for the base Lightblue metadata + * service. + * @param {Object=} options Configuration for http client. + * @param {HttpClient} http An http client. + */ +function getClient(dataHost, metadataHost, options) { + // metadataHost and options are optional + if (typeof metadataHost != "string") { + if (typeof metadataHost == "object") { + options = metadataHost; + } + dataHost = resolve(dataHost, "data"); metadataHost = resolve(dataHost, "metadata"); } - var dataClient = getDataClient(dataHost); - var metadataClient = getMetadataClient(metadataHost); + var dataClient = getDataClient(dataHost, options); + var metadataClient = getMetadataClient(metadataHost, options); return new LightblueClient(dataClient, metadataClient); +} + +function getHttpClient(options) { + return new NodeHttpClient(options); } \ No newline at end of file diff --git a/lib/metadata.js b/lib/metadata.js index d9be7cd..61c5f04 100644 --- a/lib/metadata.js +++ b/lib/metadata.js @@ -1,5 +1,10 @@ -var util = require("./clientutil"); -var RestRequest = require("./rest").RestRequest; +var util = require("./clientutil"); +var http = require("./http"); + +var isEmpty = util.isEmpty; +var resolve = util.resolve; + +var HttpRequest = http.HttpRequest; module.exports = LightblueMetadataClient; @@ -11,17 +16,22 @@ module.exports = LightblueMetadataClient; * deployed under a particular context (like /rest/data), this must be * included in the host. */ -function LightblueMetadataClient(host) { - this.host = host; +function LightblueMetadataClient(httpClient, host) { + this._httpClient = httpClient; + this._host = host; } +LightblueMetadataClient.prototype._execute = function(request) { + return this._httpClient.execute(request); +}; + /** * Request entity names of certain statuses. * @param {String[]} [statuses] An optional array of statuses to reduce the * search to. */ LightblueMetadataClient.prototype.getNames = function(statuses) { - return new NamesRequest(this.host, statuses); + return this._execute(new NamesRequest(this._host, statuses)); }; /** @@ -29,7 +39,7 @@ LightblueMetadataClient.prototype.getNames = function(statuses) { * @param {String} entityName */ LightblueMetadataClient.prototype.getVersions = function(entityName) { - return new VersionsRequest(this.host, entityName); + return this._execute(new VersionsRequest(this._host, entityName)); }; /** @@ -38,7 +48,7 @@ LightblueMetadataClient.prototype.getVersions = function(entityName) { * @param {String} version Version (required). */ LightblueMetadataClient.prototype.getMetadata = function(entityName, version) { - return new MetadataRequest(this.host, entityName, version); + return this._execute(new MetadataRequest(this._host, entityName, version)); }; /** @@ -48,7 +58,7 @@ LightblueMetadataClient.prototype.getMetadata = function(entityName, version) { * @param {String} [version] */ LightblueMetadataClient.prototype.getRoles = function(entityName, version) { - return new RolesRequest(this.host, entityName, version); + return this._execute(new RolesRequest(this._host, entityName, version)); }; function NamesRequest(host, statuses) { @@ -56,34 +66,34 @@ function NamesRequest(host, statuses) { ? "s=" + statuses.join(",") : ""; - RestRequest.call(this, "get", util.resolve(host, query)); + HttpRequest.call(this, "get", resolve(host, query)); } -NamesRequest.prototype = Object.create(RestRequest.prototype); +NamesRequest.prototype = Object.create(HttpRequest.prototype); NamesRequest.prototype.constructor = NamesRequest; function VersionsRequest(host, entityName) { - if (util.isEmpty(entityName)) { + if (isEmpty(entityName)) { throw new Error("entityName required for versions request.") } - RestRequest.call(this, "get", util.resolve(host, entityName)); + HttpRequest.call(this, "get", resolve(host, entityName)); } -VersionsRequest.prototype = Object.create(RestRequest.prototype); +VersionsRequest.prototype = Object.create(HttpRequest.prototype); VersionsRequest.prototype.constructor = VersionsRequest; function RolesRequest(host, entityName, version) { - RestRequest.call(this, "get", util.resolve(host, entityName, version, "roles")); + HttpRequest.call(this, "get", resolve(host, entityName, version, "roles")); } -RolesRequest.prototype = Object.create(RestRequest.prototype); +RolesRequest.prototype = Object.create(HttpRequest.prototype); RolesRequest.prototype.constructor = RolesRequest; function MetadataRequest(host, entityName, version) { - if (util.isEmpty(entityName) || util.isEmpty(version)) { + if (isEmpty(entityName) || isEmpty(version)) { throw new Error("entityName and version required for metadata request.") } - RestRequest.call(this, "get", util.resolve(host, entityName, version)); + HttpRequest.call(this, "get", resolve(host, entityName, version)); } \ No newline at end of file diff --git a/lib/nghttp.js b/lib/nghttp.js new file mode 100644 index 0000000..389a5ae --- /dev/null +++ b/lib/nghttp.js @@ -0,0 +1,13 @@ +module.exports = NgHttpClient; + +// TODO: Use $httpBackend instead? +function NgHttpClient($http) { + this._$http = $http; +} + +/** + * @param {RestRequest} req + */ +NgHttpClient.prototype.execute = function(req) { + return this._$http[req.method](req.url, req.body); +} \ No newline at end of file diff --git a/lib/nglightblue.js b/lib/nglightblue.js new file mode 100644 index 0000000..5400a92 --- /dev/null +++ b/lib/nglightblue.js @@ -0,0 +1,64 @@ +/* globals angular */ + +/** + * @file Entry-point for browserified module which sets up an angular module if + * angular global is present. + * + *

Usage: + *


+ * myModule.config("lightblueProvider", function(lightblueProvider) {
+ *   lightblueProvider.setDataHost("http://my.lightblue.com/data");
+ *   lightblueProvider.setMetadataHost("http://my.lightblue.com/metadata");
+ * 
+ *   // Above is equivalent to:
+ *   // lightblueProvider.setHost("http://my.lightblue.com");
+ * }
+ * 
+ * myModule.controller("foo", ["lightblue", "$scope", function(lightblue, $scope) {
+ *   lightblue.data.find(...)
+ *       .then(function(response) {
+ *         $scope.myEntity = response.processed[0];
+ *       });
+ * }
+ * 
+ */ + +var LightblueClient = require("./client.js"); +var DataClient = require("./data.js"); +var MetadataClient = require("./metadata.js"); +var NgHttpClient = require("./nghttp.js"); +var url = require("url"); + +if (typeof angular == "object" && angular != null && "module" in angular) { + angular.module("lightblue", []) + .provider("lightblue", LightblueProvider); +} else { + console.warn("nglightblue loaded but angular is not. Make sure angular is " + + "loaded first."); +} + +function LightblueProvider() { + this._dataHost = ""; + this._metadataHost = ""; +} + +LightblueProvider.prototype.setHost = function(host) { + this.setDataHost(url.resolve(host, "/data")); + this.setMetadataHost(url.resolve(host, "/metadata")); +}; + +LightblueProvider.prototype.setDataHost = function(host) { + this._dataHost = host; +}; + +LightblueProvider.prototype.setMetadataHost = function(host) { + this._metadataHost = host; +}; + +LightblueProvider.prototype.$get = ["$http", function($http) { + var httpClient = new NgHttpClient($http); + var dataClient = new DataClient(httpClient, this._dataHost); + var metadataClient = new MetadataClient(httpClient, this._metadataHost); + + return new LightblueClient(dataClient, metadataClient); +}]; \ No newline at end of file diff --git a/lib/nodehttp.js b/lib/nodehttp.js new file mode 100644 index 0000000..651c423 --- /dev/null +++ b/lib/nodehttp.js @@ -0,0 +1,91 @@ +var q = require("q"); +var http = require("http"); +var https = require("https"); +var url = require("url"); +var util = require("./clientutil"); + +var isDefined = util.isDefined; +var ifDefined = util.ifDefined; + +// TODO: What is convention / best practice with exports? +module.exports = NodeHttpClient; + +/** + * @param {String} options.auth.user Username to use for basic auth + * @param {String} options.auth.pass Password to use for basic auth + */ +function NodeHttpClient(options) { + this._exec = http.request; + this._execSsl = https.request; + + if (options instanceof Object) { + this._auth = ifDefined(options.auth, null); + // TODO: Support ssl certs + } +} + +/** + * @param {HttpRequest} requestOpts + */ +NodeHttpClient.prototype.execute = function(requestOpts) { + var deferred = q.defer(); + var promise = deferred.promise; + + var exec = requestOpts.url.indexOf("https") === 0 + ? this._execSsl + : this._exec; + + var body = isDefined(requestOpts.body) + ? JSON.stringify(requestOpts.body) + : ""; + + var options = url.parse(requestOpts.url); + options.method = requestOpts.METHOD; + options.headers = { + "Content-Type": "application/json", + "Content-Length": body.length + }; + + if (this._auth != null) { + options.auth = this._auth; + } + + var request = exec(options, function(res) { + var responseBody = ""; + + res.setEncoding("utf8"); + + res.on("data", function(chunk) { + responseBody += chunk; + }); + + res.on("end", function() { + // TODO: resolve with more than just body? + // TODO: error on lightblue error? + deferred.resolve(responseBody); + }); + + res.on("error", function(error) { + // TODO: what is this resolving with? + deferred.reject(error); + }); + }); + + request.on("error", function(error) { + // TODO: reject with something consistent + deferred.reject(error); + }); + + request.write(body, "utf8"); + request.end(); + + promise.success = function(callback) { + return promise.then(callback); + }; + + promise.error = function(callback) { + return promise.then(null, callback); + }; + + return promise; +}; \ No newline at end of file diff --git a/lib/rest.js b/lib/rest.js deleted file mode 100644 index 4586f2b..0000000 --- a/lib/rest.js +++ /dev/null @@ -1,6 +0,0 @@ -exports.RestRequest = function(method, url, body) { - this.method = (typeof method === "string") ? method.toLowerCase() : ""; - this.METHOD = this.method.toUpperCase(); - this.body = body; - this.url = url; -} \ No newline at end of file diff --git a/package.json b/package.json index 0337def..0d60abd 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,28 @@ { "name": "lightblue.js", - "version": "0.4.1", + "version": "0.5.0-alpha", "description": "A lightblue client for javascript.", + "repository": { + "type": "git", + "url": "https://github.com/alechenninger/lightblue.js.git" + }, + "main": "lib/lightblue.js", "scripts": { - "package": "browserify lib/lightblue.js --standalone lightblue -d -p [minifyify --map lightblue.map.json --output dist/lightblue.map.json] > dist/lightblue.min.js", - "watch": "watchify lib/lightblue.js --standalone lightblue -d -p [minifyify --map lightblue.map.json --output dist/lightblue.map.json] -o dist/lightblue.min.js", + "bundle": "browserify lib/lightblue.js --standalone lightblue -d -p [minifyify --map lightblue.map.json --output dist/lightblue.map.json] > dist/lightblue.min.js", "test": "mocha test/*", "tdd": "mocha -w test/*" }, + "dependencies": { + "q": "^1.0.0" + }, "devDependencies": { + "angular": "^1.3.15", + "benv": "^1.1.0", "browserify": "^6.2.0", "chai": "^1.9.2", "minifyify": "^4.4.0", "mocha": "^2.0.1", + "nock": "^1.7.0", "watchify": "^2.1.0" } } diff --git a/test/data.js b/test/data_test.js similarity index 63% rename from test/data.js rename to test/data_test.js index fb704f0..0df07d0 100644 --- a/test/data.js +++ b/test/data_test.js @@ -1,25 +1,36 @@ +/* globals describe it */ var expect = require("chai").expect; -var client = require("../lib/lightblue").getDataClient; +var DataClient = require("../lib/data.js"); describe("LightblueDataClient", function() { + + // Captures request sent to execute(req) + var mockHttpClient = { + execute: function(request) { + this.request = request; + return "response"; + } + }; + + var dataClient = new DataClient(mockHttpClient, "myhost.com"); + describe("find", function() { it("should construct urls like ${host}/find/${entity}/${version}", function() { - var findRequest = client("myhost.com").find(validFindConfig({ + dataClient.find(validFindConfig({ entity: "myEntity", version: "myVersion" })); - expect(findRequest.url).to.match(new RegExp("^myhost.com/find/myEntity/myVersion/?$")); + expect(mockHttpClient.request.url).to.match(new RegExp("^myhost.com/find/myEntity/myVersion/?$")); }); it("should construct urls like ${host}/find/${entity} when version is undefined", function () { - var config = validFindConfig({ - entity: "myEntity"}); + var config = validFindConfig({entity: "myEntity"}); delete config.version; - var findRequest = client("myhost.com").find(config); + dataClient.find(config); - expect(findRequest.url).to.match(new RegExp("^myhost.com/find/myEntity/?$")); + expect(mockHttpClient.request.url).to.match(new RegExp("^myhost.com/find/myEntity/?$")); }); it("should construct urls like ${host}/find/${entity} when version is empty string", function () { @@ -28,15 +39,15 @@ describe("LightblueDataClient", function() { version: "" }); - var findRequest = client("myhost.com").find(config); + dataClient.find(config); - expect(findRequest.url).to.match(new RegExp("^myhost.com/find/myEntity/?$")); + expect(mockHttpClient.request.url).to.match(new RegExp("^myhost.com/find/myEntity/?$")); }); it("should use POST", function() { - var findRequest = client("myhost.com").find(validFindConfig()); + dataClient.find(validFindConfig()); - expect(findRequest.method).to.equal("post"); + expect(mockHttpClient.request.method).to.equal("post"); }); it("should construct request body with objectType, version, query and projection", function() { @@ -54,8 +65,9 @@ describe("LightblueDataClient", function() { projection: expectedBody.projection }; - var findRequest = client("myhost.com").find(config); - expect(findRequest.body).to.deep.equal(expectedBody); + dataClient.find(config); + + expect(mockHttpClient.request.body).to.deep.equal(expectedBody); }); it("should construct request body with objectType, version, query, projection, sort, and range", function() { @@ -77,37 +89,44 @@ describe("LightblueDataClient", function() { range: expectedBody.range }; - var findRequest = client("myhost.com").find(config); - expect(findRequest.body).to.deep.equal(expectedBody); + dataClient.find(config); + + expect(mockHttpClient.request.body).to.deep.equal(expectedBody); }); it("should allow expressing range as an object with from and to properties", function() { - var findRequest = client("myhost.com").find(validFindConfig({ + dataClient.find(validFindConfig({ range: {from: 1, to: 10} })); - expect(findRequest.body.range[0]).to.equal(1); - expect(findRequest.body.range[1]).to.equal(10); + expect(mockHttpClient.request.body.range[0]).to.equal(1); + expect(mockHttpClient.request.body.range[1]).to.equal(10); + }); + + it("should return result of http client execute", function() { + var response = dataClient.find(validFindConfig()); + + expect(response).to.equal("response"); }); }); describe("insert", function() { it("should construct urls like ${host}/insert/${entity}/${version}", function() { - var insertRequest = client("myhost.com").insert(validInsertConfig({ + dataClient.insert(validInsertConfig({ entity: "myEntity", version: "1.2.0" })); - expect(insertRequest.url).to.match(new RegExp("^myhost.com/insert/myEntity/1.2.0/?$")); + expect(mockHttpClient.request.url).to.match(new RegExp("^myhost.com/insert/myEntity/1.2.0/?$")); }); it("should construct urls like ${host}/insert/${entity} when version is undefined", function () { var config = validInsertConfig({entity: "myEntity"}); delete config.version; - var insertRequest = client("myhost.com").insert(config); + dataClient.insert(config); - expect(insertRequest.url).to.match(new RegExp("^myhost.com/insert/myEntity/?$")); + expect(mockHttpClient.request.url).to.match(new RegExp("^myhost.com/insert/myEntity/?$")); }); it("should construct urls like ${host}/insert/${entity} when version is empty string", function () { @@ -116,15 +135,15 @@ describe("LightblueDataClient", function() { version: "" }); - var insertRequest = client("myhost.com").insert(config); + dataClient.insert(config); - expect(insertRequest.url).to.match(new RegExp("^myhost.com/insert/myEntity/?$")); + expect(mockHttpClient.request.url).to.match(new RegExp("^myhost.com/insert/myEntity/?$")); }); it("should use PUT", function() { - var insertRequest = client("myhost.com").insert(validInsertConfig()); + dataClient.insert(validInsertConfig()); - expect(insertRequest.method).to.equal("put"); + expect(mockHttpClient.request.method).to.equal("put"); }); it("should include request data", function() { @@ -142,29 +161,35 @@ describe("LightblueDataClient", function() { projection: expectedBody.projection }; - var insertRequest = client("myhost.com").insert(config); + dataClient.insert(config); - expect(insertRequest.body).to.deep.equal(expectedBody); + expect(mockHttpClient.request.body).to.deep.equal(expectedBody); + }); + + it("should return result of http client execute", function() { + var response = dataClient.insert(validInsertConfig()); + + expect(response).to.equal("response"); }); }); describe("update", function() { it("should construct urls like ${host}/update/${entity}/${version}", function() { - var updateRequest = client("myhost.com").update(validUpdateConfig({ + dataClient.update(validUpdateConfig({ entity: "myEntity", version: "1.2.0" })); - expect(updateRequest.url).to.match(new RegExp("^myhost.com/update/myEntity/1.2.0/?$")); + expect(mockHttpClient.request.url).to.match(new RegExp("^myhost.com/update/myEntity/1.2.0/?$")); }); it("should construct urls like ${host}/update/${entity} when version is undefined", function () { var config = validUpdateConfig({entity: "myEntity"}); delete config.version; - var updateRequest = client("myhost.com").update(config); + dataClient.update(config); - expect(updateRequest.url).to.match(new RegExp("^myhost.com/update/myEntity/?$")); + expect(mockHttpClient.request.url).to.match(new RegExp("^myhost.com/update/myEntity/?$")); }); it("should construct urls like ${host}/update/${entity} when version is empty string", function () { @@ -173,15 +198,15 @@ describe("LightblueDataClient", function() { version: "" }); - var updateRequest = client("myhost.com").update(config); + dataClient.update(config); - expect(updateRequest.url).to.match(new RegExp("^myhost.com/update/myEntity/?$")); + expect(mockHttpClient.request.url).to.match(new RegExp("^myhost.com/update/myEntity/?$")); }); it("should use POST", function() { - var updateRequest = client("myhost.com").update(validUpdateConfig()); + dataClient.update(validUpdateConfig()); - expect(updateRequest.method).to.equal("post"); + expect(mockHttpClient.request.method).to.equal("post"); }); it("should include request data", function() { @@ -201,29 +226,35 @@ describe("LightblueDataClient", function() { projection: expectedBody.projection }; - var updateRequest = client("myhost.com").update(config); + dataClient.update(config); - expect(updateRequest.body).to.deep.equal(expectedBody); + expect(mockHttpClient.request.body).to.deep.equal(expectedBody); + }); + + it("should return result of http client execute", function() { + var response = dataClient.update(validUpdateConfig()); + + expect(response).to.equal("response"); }); }); describe("save", function() { it("should construct urls like ${host}/save/${entity}/${version}", function() { - var saveRequest = client("myhost.com").save(validSaveConfig({ + dataClient.save(validSaveConfig({ entity: "myEntity", version: "1.2.0" })); - expect(saveRequest.url).to.match(new RegExp("^myhost.com/save/myEntity/1.2.0/?$")); + expect(mockHttpClient.request.url).to.match(new RegExp("^myhost.com/save/myEntity/1.2.0/?$")); }); it("should construct urls like ${host}/save/${entity} when version is undefined", function () { var config = validSaveConfig({entity: "myEntity"}); delete config.version; - var saveRequest = client("myhost.com").save(config); + dataClient.save(config); - expect(saveRequest.url).to.match(new RegExp("^myhost.com/save/myEntity/?$")); + expect(mockHttpClient.request.url).to.match(new RegExp("^myhost.com/save/myEntity/?$")); }); it("should construct urls like ${host}/save/${entity} when version is empty string", function () { @@ -232,15 +263,15 @@ describe("LightblueDataClient", function() { version: "" }); - var saveRequest = client("myhost.com").save(config); + dataClient.save(config); - expect(saveRequest.url).to.match(new RegExp("^myhost.com/save/myEntity/?$")); + expect(mockHttpClient.request.url).to.match(new RegExp("^myhost.com/save/myEntity/?$")); }); it("should use POST", function() { - var saveRequest = client("myhost.com").save(validSaveConfig()); + dataClient.save(validSaveConfig()); - expect(saveRequest.method).to.equal("post"); + expect(mockHttpClient.request.method).to.equal("post"); }); it("should include request data", function() { @@ -260,29 +291,35 @@ describe("LightblueDataClient", function() { upsert: expectedBody.upsert }; - var saveRequest = client("myhost.com").save(config); + dataClient.save(config); - expect(saveRequest.body).to.deep.equal(expectedBody); + expect(mockHttpClient.request.body).to.deep.equal(expectedBody); + }); + + it("should return result of http client execute", function() { + var response = dataClient.save(validSaveConfig()); + + expect(response).to.equal("response"); }); }); describe("delete", function() { it("should construct urls like ${host/data/delete/${entity}/${version}", function() { - var deleteRequest = client("myhost.com").delete(validDeleteConfig({ + dataClient.delete(validDeleteConfig({ entity: "myEntity", version: "1.2.0" })); - expect(deleteRequest.url).to.match(new RegExp("^myhost.com/delete/myEntity/1.2.0/?$")); + expect(mockHttpClient.request.url).to.match(new RegExp("^myhost.com/delete/myEntity/1.2.0/?$")); }); it("should construct urls like ${host}/delete/${entity} when version is undefined", function () { var config = validDeleteConfig({entity: "myEntity"}); delete config.version; - var deleteRequest = client("myhost.com").delete(config); + dataClient.delete(config); - expect(deleteRequest.url).to.match(new RegExp("^myhost.com/delete/myEntity/?$")); + expect(mockHttpClient.request.url).to.match(new RegExp("^myhost.com/delete/myEntity/?$")); }); it("should construct urls like ${host}/delete/${entity} when version is empty string", function () { @@ -291,15 +328,15 @@ describe("LightblueDataClient", function() { version: "" }); - var deleteRequest = client("myhost.com").delete(config); + dataClient.delete(config); - expect(deleteRequest.url).to.match(new RegExp("^myhost.com/delete/myEntity/?$")); + expect(mockHttpClient.request.url).to.match(new RegExp("^myhost.com/delete/myEntity/?$")); }); it("should use POST", function() { - var deleteRequest = client("myhost.com").delete(validDeleteConfig()); + dataClient.delete(validDeleteConfig()); - expect(deleteRequest.method).to.equal("post"); + expect(mockHttpClient.request.method).to.equal("post"); }); it("should include request data", function() { @@ -307,7 +344,7 @@ describe("LightblueDataClient", function() { objectType: "wizard", version: "1.0.0", query: {field: "name", op: "=", rvalue: "Voldemort"} - } + }; var config = { entity: expectedBody.objectType, @@ -315,12 +352,17 @@ describe("LightblueDataClient", function() { query: expectedBody.query }; - var deleteRequest = client("myhost.com").delete(config); + dataClient.delete(config); - expect(deleteRequest.body).to.deep.equal(expectedBody); + expect(mockHttpClient.request.body).to.deep.equal(expectedBody); + }); + + it("should return result of http client execute", function() { + var response = dataClient.delete(validDeleteConfig()); + + expect(response).to.equal("response"); }); }); - }); function validFindConfig(edit) { diff --git a/test/nghttp_test.js b/test/nghttp_test.js new file mode 100644 index 0000000..a9326d6 --- /dev/null +++ b/test/nghttp_test.js @@ -0,0 +1,22 @@ +/* globals describe it */ +var expect = require("chai").expect; +var NgHttpClient = require("../lib/nghttp.js"); +var HttpRequest = require("../lib/http.js").HttpRequest; + +describe("NgHttpClient", function() { + it("performs request with correct method, url, and body", function() { + var mock$Http = { + post: function(url, body) { + this.url = url; + this.body = body; + } + }; + + var ngHttp = new NgHttpClient(mock$Http); + + ngHttp.execute(new HttpRequest("post", "http://foo", {foo: "bar"})); + + expect(mock$Http.url).to.equal("http://foo"); + expect(mock$Http.body).to.deep.equal({foo: "bar"}); + }); +}); \ No newline at end of file diff --git a/test/nglightblue_test.js b/test/nglightblue_test.js new file mode 100644 index 0000000..ee9b294 --- /dev/null +++ b/test/nglightblue_test.js @@ -0,0 +1,60 @@ +/* globals describe it beforeEach afterEach angular */ +var benv = require("benv"); +var expect = require("chai").expect; + +describe("nglightblue", function() { + var testModule; + + describe("with angular frontend", function() { + beforeEach("setup browser environment", function(done) { + benv.setup(function() { + benv.expose({ + angular: benv.require("../node_modules/angular/angular.js", "angular") + }); + benv.require("../lib/nglightblue.js"); + done(); + }); + }); + + beforeEach("create angular module", function() { + testModule = angular.module("test", ["lightblue"]); + }); + + afterEach("remove browser environment", function() { + benv.teardown(); + }); + + it("registers a lightblueProvider", function(done) { + testModule.config(["lightblueProvider", function(lightblueProvider) { + expect(lightblueProvider).not.to.be.null; + done(); + }]); + + angular.bootstrap(document, ["test"]); + }); + + it("uses $http", function(done) { + var mock$Http = "I'm not really $http ssshhhh"; + + testModule.factory("$http", function() { + return mock$Http; + }); + + testModule.config(["lightblueProvider", function(lightblueProvider) { + lightblueProvider.$get = ["$http", function($http) { + expect($http).to.equal(mock$Http); + done(); + }]; + }]); + + testModule.run(["lightblue", function(lightblue) {}]); + + angular.bootstrap(document, ["test"]); + }); + }); + + it("doesn't get mad if angular is not loaded", function() { + require("../lib/nglightblue.js"); + // If require does not fail, we're good + }); +}); \ No newline at end of file diff --git a/test/nodehttp_test.js b/test/nodehttp_test.js new file mode 100644 index 0000000..b43634f --- /dev/null +++ b/test/nodehttp_test.js @@ -0,0 +1,58 @@ +/* globals describe it before */ +var NodeHttpClient = require("../lib/nodehttp.js"); +var HttpRequest = require("../lib/http.js").HttpRequest; +var nock = require("nock"); +var expect = require("chai").expect; + +describe("NodeHttpClient", function() { + var client; + + before(function() { + client = new NodeHttpClient(); + }); + + it("makes a get request against url with path", function(done) { + nock("http://foo.com") + .get("/api") + .reply(200); + + client.execute(new HttpRequest("get", "http://foo.com/api")) + .then(function(response) { + done(); + }); + }); + + it("makes a get requst against url without path", function(done) { + nock("http://foo.com") + .get("/") + .reply(200); + + client.execute(new HttpRequest("get", "http://foo.com")) + .then(function(response) { + done(); + }); + }); + + it("makes a post request with json body", function(done) { + nock("http://foo.com") + .post("/", {foo: "bar"}) + .reply(200); + + client.execute(new HttpRequest("post", "http://foo.com", {foo: "bar"})) + .then(function(response) { + done(); + }); + }); + + it("makes request and completes promise with success response body", function(done) { + nock("http://foo.com") + .get("/") + .reply(200, "it works"); + + client.execute(new HttpRequest("get", "http://foo.com")) + .then(function(response) { + expect(response).to.equal("it works"); + done(); + }); + }); +}); \ No newline at end of file diff --git a/test/query.js b/test/query_test.js similarity index 100% rename from test/query.js rename to test/query_test.js