diff --git a/CHANGES.md b/CHANGES.md index 97405529..e1806b9c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,35 @@ +2016-02-26, Version 2.26.0 +========================== + + * Add support for "file" return args (Miroslav Bajtoš) + + * Fix handling of args with http.target:header (Miroslav Bajtoš) + + +2016-02-09, Version 2.25.1 +========================== + + * Back-port pull request #283 from strongloop/fix/afterError (Miroslav Bajtoš) + + * Do not call "afterError" on success (Miroslav Bajtoš) + + +2016-02-02, Version 2.25.0 +========================== + + * Backport pull request #279 from strongloop/feature/phases (Miroslav Bajtoš) + + * Add remotes.registerPhaseHandler (Miroslav Bajtoš) + + * Add remote invocation phases (Miroslav Bajtoš) + + * Simplify invokeMethodInContext (Miroslav Bajtoš) + + * Extract ContextBase and ctx.getScope() (Miroslav Bajtoš) + + * Customize XML root element in remoteMethod (David Cheung) + + 2016-01-11, Version 2.24.0 ========================== @@ -531,8 +563,6 @@ 2014-06-13, Version 2.0.0-beta4 =============================== - * 2.0.0-beta4 (Miroslav Bajtoš) - * Add more classes to JSDocs; some doc cleanup. (crandmck) * !fixup use strongloop/node.js literal convention (Ritchie Martori) @@ -593,8 +623,6 @@ 2014-06-03, Version 2.0.0-beta3 =============================== - * 2.0.0-beta3 (Miroslav Bajtoš) - * Exclude body-parser from the browser bundle (Miroslav Bajtoš) * Remove duplicate dep (Raymond Feng) diff --git a/Gruntfile.js b/Gruntfile.js index 7d60b876..15bcbbed 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + /*global module:false*/ module.exports = function(grunt) { diff --git a/LICENSE.md b/LICENSE.md index 35f5f930..165f8601 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,9 +1,210 @@ -Copyright (c) 2013-2015 StrongLoop, Inc. +Copyright (c) IBM Corp. 2013,2016. All Rights Reserved. +Node module: strong-remoting +This project is licensed under the Artistic License 2.0, full text below. -strong-remoting uses a dual license model. +-------- -You may use this library under the terms of the [Artistic 2.0 license][], -or under the terms of the [StrongLoop Subscription Agreement][]. +The Artistic License 2.0 -[Artistic 2.0 license]: http://opensource.org/licenses/Artistic-2.0 -[StrongLoop Subscription Agreement]: http://strongloop.com/license +Copyright (c) 2000-2006, The Perl Foundation. + +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + +Preamble + +This license establishes the terms under which a given free software +Package may be copied, modified, distributed, and/or redistributed. +The intent is that the Copyright Holder maintains some artistic +control over the development of that Package while still keeping the +Package available as open source and free software. + +You are always permitted to make arrangements wholly outside of this +license directly with the Copyright Holder of a given Package. If the +terms of this license do not permit the full use that you propose to +make of the Package, you should contact the Copyright Holder and seek +a different licensing arrangement. + +Definitions + + "Copyright Holder" means the individual(s) or organization(s) + named in the copyright notice for the entire Package. + + "Contributor" means any party that has contributed code or other + material to the Package, in accordance with the Copyright Holder's + procedures. + + "You" and "your" means any person who would like to copy, + distribute, or modify the Package. + + "Package" means the collection of files distributed by the + Copyright Holder, and derivatives of that collection and/or of + those files. A given Package may consist of either the Standard + Version, or a Modified Version. + + "Distribute" means providing a copy of the Package or making it + accessible to anyone else, or in the case of a company or + organization, to others outside of your company or organization. + + "Distributor Fee" means any fee that you charge for Distributing + this Package or providing support for this Package to another + party. It does not mean licensing fees. + + "Standard Version" refers to the Package if it has not been + modified, or has been modified only in ways explicitly requested + by the Copyright Holder. + + "Modified Version" means the Package, if it has been changed, and + such changes were not explicitly requested by the Copyright + Holder. + + "Original License" means this Artistic License as Distributed with + the Standard Version of the Package, in its current version or as + it may be modified by The Perl Foundation in the future. + + "Source" form means the source code, documentation source, and + configuration files for the Package. + + "Compiled" form means the compiled bytecode, object code, binary, + or any other form resulting from mechanical transformation or + translation of the Source form. + + +Permission for Use and Modification Without Distribution + +(1) You are permitted to use the Standard Version and create and use +Modified Versions for any purpose without restriction, provided that +you do not Distribute the Modified Version. + + +Permissions for Redistribution of the Standard Version + +(2) You may Distribute verbatim copies of the Source form of the +Standard Version of this Package in any medium without restriction, +either gratis or for a Distributor Fee, provided that you duplicate +all of the original copyright notices and associated disclaimers. At +your discretion, such verbatim copies may or may not include a +Compiled form of the Package. + +(3) You may apply any bug fixes, portability changes, and other +modifications made available from the Copyright Holder. The resulting +Package will still be considered the Standard Version, and as such +will be subject to the Original License. + + +Distribution of Modified Versions of the Package as Source + +(4) You may Distribute your Modified Version as Source (either gratis +or for a Distributor Fee, and with or without a Compiled form of the +Modified Version) provided that you clearly document how it differs +from the Standard Version, including, but not limited to, documenting +any non-standard features, executables, or modules, and provided that +you do at least ONE of the following: + + (a) make the Modified Version available to the Copyright Holder + of the Standard Version, under the Original License, so that the + Copyright Holder may include your modifications in the Standard + Version. + + (b) ensure that installation of your Modified Version does not + prevent the user installing or running the Standard Version. In + addition, the Modified Version must bear a name that is different + from the name of the Standard Version. + + (c) allow anyone who receives a copy of the Modified Version to + make the Source form of the Modified Version available to others + under + + (i) the Original License or + + (ii) a license that permits the licensee to freely copy, + modify and redistribute the Modified Version using the same + licensing terms that apply to the copy that the licensee + received, and requires that the Source form of the Modified + Version, and of any works derived from it, be made freely + available in that license fees are prohibited but Distributor + Fees are allowed. + + +Distribution of Compiled Forms of the Standard Version +or Modified Versions without the Source + +(5) You may Distribute Compiled forms of the Standard Version without +the Source, provided that you include complete instructions on how to +get the Source of the Standard Version. Such instructions must be +valid at the time of your distribution. If these instructions, at any +time while you are carrying out such distribution, become invalid, you +must provide new instructions on demand or cease further distribution. +If you provide valid instructions or cease distribution within thirty +days after you become aware that the instructions are invalid, then +you do not forfeit any of your rights under this license. + +(6) You may Distribute a Modified Version in Compiled form without +the Source, provided that you comply with Section 4 with respect to +the Source of the Modified Version. + + +Aggregating or Linking the Package + +(7) You may aggregate the Package (either the Standard Version or +Modified Version) with other packages and Distribute the resulting +aggregation provided that you do not charge a licensing fee for the +Package. Distributor Fees are permitted, and licensing fees for other +components in the aggregation are permitted. The terms of this license +apply to the use and Distribution of the Standard or Modified Versions +as included in the aggregation. + +(8) You are permitted to link Modified and Standard Versions with +other works, to embed the Package in a larger work of your own, or to +build stand-alone binary or bytecode versions of applications that +include the Package, and Distribute the result without restriction, +provided the result does not expose a direct interface to the Package. + + +Items That are Not Considered Part of a Modified Version + +(9) Works (including, but not limited to, modules and scripts) that +merely extend or make use of the Package, do not, by themselves, cause +the Package to be a Modified Version. In addition, such works are not +considered parts of the Package itself, and are not subject to the +terms of this license. + + +General Provisions + +(10) Any use, modification, and distribution of the Standard or +Modified Versions is governed by this Artistic License. By using, +modifying or distributing the Package, you accept this license. Do not +use, modify, or distribute the Package, if you do not accept this +license. + +(11) If your Modified Version has been derived from a Modified +Version made by someone other than you, you are nevertheless required +to ensure that your Modified Version complies with the requirements of +this license. + +(12) This license does not grant you the right to use any trademark, +service mark, tradename, or logo of the Copyright Holder. + +(13) This license includes the non-exclusive, worldwide, +free-of-charge patent license to make, have made, use, offer to sell, +sell, import and otherwise transfer the Package with respect to any +patent claims licensable by the Copyright Holder that are necessarily +infringed by the Package. If you institute patent litigation +(including a cross-claim or counterclaim) against any party alleging +that the Package constitutes direct or contributory patent +infringement, then this Artistic License to you shall terminate on the +date that such litigation is filed. + +(14) Disclaimer of Warranty: +THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS +IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR +NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL +LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +-------- diff --git a/example/before-after.js b/example/before-after.js index e119f620..c7ac3ddc 100644 --- a/example/before-after.js +++ b/example/before-after.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + // create a set of shared classes var remotes = require('../').create(); diff --git a/example/documentation/index.js b/example/documentation/index.js index 4baef5a5..b8eea3ac 100644 --- a/example/documentation/index.js +++ b/example/documentation/index.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + var http = require('http'); var remotes = require('./remotes'); var meta = require('../../ext/meta'); diff --git a/example/documentation/remotes/contract-class.js b/example/documentation/remotes/contract-class.js index 3283d868..e2280b56 100644 --- a/example/documentation/remotes/contract-class.js +++ b/example/documentation/remotes/contract-class.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + // This example shows using the helper for a type in a "definitive" fashion. var helper = require('../../../').extend(module.exports); var clshelper; diff --git a/example/documentation/remotes/contract.js b/example/documentation/remotes/contract.js index f0c91bd2..52f016e1 100644 --- a/example/documentation/remotes/contract.js +++ b/example/documentation/remotes/contract.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + var helper = require('../../../').extend(module.exports); /** diff --git a/example/documentation/remotes/index.js b/example/documentation/remotes/index.js index fd8324a2..21c17f59 100644 --- a/example/documentation/remotes/index.js +++ b/example/documentation/remotes/index.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + var remotes = require('../../../').create(); /** diff --git a/example/documentation/remotes/simple-class.js b/example/documentation/remotes/simple-class.js index d4b8e735..950c9ef9 100644 --- a/example/documentation/remotes/simple-class.js +++ b/example/documentation/remotes/simple-class.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + // This example shows using the helper for a type in a "post-definition" style. var helper = require('../../../').extend(module.exports); diff --git a/example/documentation/remotes/simple.js b/example/documentation/remotes/simple.js index 7057073b..4902bcc9 100644 --- a/example/documentation/remotes/simple.js +++ b/example/documentation/remotes/simple.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + // This helper adds methods to a module that we assume will be added to the remotes. // TODO(schoon) - Make this _the_ API, not a "helper". // TODO(schoon) - Document EVERYTHING diff --git a/example/remote-fs.js b/example/remote-fs.js index 4eebd787..e234b847 100644 --- a/example/remote-fs.js +++ b/example/remote-fs.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + // create a set of shared classes var remotes = require('../').create(); diff --git a/example/root.js b/example/root.js index e12d1fd6..e2ecdef3 100644 --- a/example/root.js +++ b/example/root.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + // create a set of shared classes var remotes = require('../').create(); diff --git a/example/shared-class.js b/example/shared-class.js index c67f5e12..104e4acd 100644 --- a/example/shared-class.js +++ b/example/shared-class.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + var express = require('express'); // define a vanilla JavaScript class diff --git a/example/simple-types.js b/example/simple-types.js index f6110483..3278c902 100644 --- a/example/simple-types.js +++ b/example/simple-types.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + // create a set of shared classes var remotes = require('../').create(); diff --git a/example/simple.js b/example/simple.js index fc28ae4f..74227a96 100644 --- a/example/simple.js +++ b/example/simple.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + // create a set of shared classes var remoting = require('../'); var SharedClass = remoting.SharedClass diff --git a/example/socket-io/client.js b/example/socket-io/client.js index 6501cbd8..99e829f5 100644 --- a/example/socket-io/client.js +++ b/example/socket-io/client.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + var Remotes = require('../../client/js/client'); var SocketIOAdapter = require('../../client/js/socket-io-adapter'); var remotes = Remotes.connect('http://localhost:3000', SocketIOAdapter); diff --git a/example/socket-io/server.js b/example/socket-io/server.js index 168b12d6..bbe9460e 100644 --- a/example/socket-io/server.js +++ b/example/socket-io/server.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + // create a set of shared classes var remotes = require('../../').create(); diff --git a/example/streams.js b/example/streams.js index afe5c09f..5551a740 100644 --- a/example/streams.js +++ b/example/streams.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + // faux remote stream var destination = process.stdout; var fs = require('fs'); diff --git a/ext/meta.js b/ext/meta.js index 9227b553..266143a3 100644 --- a/ext/meta.js +++ b/ext/meta.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + /** * Expose the `Meta` plugin. */ diff --git a/index.js b/index.js index 4f896e10..a727b1f9 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + /** * remotes ~ public api */ diff --git a/lib/context-base.js b/lib/context-base.js new file mode 100644 index 00000000..b41d2e7a --- /dev/null +++ b/lib/context-base.js @@ -0,0 +1,31 @@ +// Copyright IBM Corp. 2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + +'use strict'; + +var EventEmitter = require('events').EventEmitter; +var inherits = require('util').inherits; + +module.exports = ContextBase; + +/** + * A base class for all Context instances + */ +function ContextBase(method) { + EventEmitter.call(this); + + this.method = method; +} + +inherits(ContextBase, EventEmitter); + +ContextBase.prototype.getScope = function() { + // Static methods are invoked on the constructor (this = constructor fn) + // Prototype methods are invoked on the instance (this = instance) + var method = this.method; + return this.instance || + method.ctor || + method.sharedMethod && method.sharedMethod.ctor; +}; diff --git a/lib/dynamic.js b/lib/dynamic.js index 2438da28..fa86bc12 100644 --- a/lib/dynamic.js +++ b/lib/dynamic.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + /** * Expose `Dynamic`. */ diff --git a/lib/exports-helper.js b/lib/exports-helper.js index e47073d7..2983242a 100644 --- a/lib/exports-helper.js +++ b/lib/exports-helper.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + /*! * Expose `ExportsHelper`. */ diff --git a/lib/http-context.js b/lib/http-context.js index 27ce93ee..9fb80577 100644 --- a/lib/http-context.js +++ b/lib/http-context.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + /*! * Expose `HttpContext`. */ @@ -8,13 +13,15 @@ module.exports = HttpContext; * Module dependencies. */ -var EventEmitter = require('events').EventEmitter; var debug = require('debug')('strong-remoting:http-context'); var util = require('util'); var inherits = util.inherits; var assert = require('assert'); +var ContextBase = require('./context-base'); var Dynamic = require('./dynamic'); var js2xmlparser = require('js2xmlparser'); +var SharedMethod = require('./shared-method'); + var DEFAULT_SUPPORTED_TYPES = [ 'application/json', 'application/javascript', 'application/xml', 'text/javascript', 'text/xml', @@ -43,6 +50,8 @@ var SSEClient = require('sse').Client; */ function HttpContext(req, res, method, options) { + ContextBase.call(this, method); + this.req = req; this.res = res; this.method = method; @@ -71,11 +80,7 @@ function HttpContext(req, res, method, options) { } } -/*! - * Inherit from `EventEmitter`. - */ - -inherits(HttpContext, EventEmitter); +inherits(HttpContext, ContextBase); HttpContext.prototype.createStream = function() { var streamsDesc = this.method.streams; @@ -395,28 +400,33 @@ HttpContext.prototype.invoke = function(scope, method, fn, isCtor) { }; HttpContext.prototype.setReturnArgByName = function(name, value) { + var ARG_WAS_HANDLED = true; var returnDesc = this.method.getReturnArgDescByName(name); var result = this.result; var res = this.res; if (!returnDesc) { - return debug('warning: cannot set return value for arg' + + debug('warning: cannot set return value for arg' + ' (%s) without description!', name); + return; } if (returnDesc.root) { - this.result = value; - } else if (returnDesc.http) { + // TODO(bajtos) call SharedMethod's convertToBasicRemotingType here? + this.resultType = typeof returnDesc.type === 'string' ? + returnDesc.type.toLowerCase() : returnDesc.type; + return; + } + + if (returnDesc.http) { switch (returnDesc.http.target) { case 'status': res.status(value); - break; + return ARG_WAS_HANDLED; case 'header': res.set(returnDesc.http.header || name, value); - break; + return ARG_WAS_HANDLED; } - } else { - result[name] = value; } }; @@ -433,19 +443,26 @@ function toJSON(input) { } } -function toXML(input) { +function toXML(input, options) { var xml; + var xmlDefaultOptions = { declaration : true }; + var xmlOptions = util._extend(xmlDefaultOptions, options); if (input && typeof input.toXML === 'function') { xml = input.toXML(); } else { - if (input) { + if (typeof input == 'string') { + // this is a hack to handle js2xmlparser throws syntax error on string + // wraps string from "a string" to "\"a string\"" + input = JSON.stringify(input); + } else if (typeof input == 'object') { // Trigger toJSON() conversions input = toJSON(input); } if (Array.isArray(input)) { input = { result: input }; } - xml = js2xmlparser('response', input, { + xml = js2xmlparser(xmlOptions.wrapperElement || 'response', input, { + declaration : { include : xmlOptions.declaration }, prettyPrinting: { indentString: ' ' }, @@ -465,7 +482,11 @@ HttpContext.prototype.shouldReturnEventStream = function() { var format = query._format; var acceptable = req.accepts('text/event-stream'); - return (format === 'event-stream') || acceptable; + var returnEventStream = (format === 'event-stream') || acceptable; + if (returnEventStream) { + this.res.setHeader('Content-Encoding', 'x-no-compression'); + } + return returnEventStream; }; HttpContext.prototype.respondWithEventStream = function(stream) { @@ -501,13 +522,14 @@ function sendBodyJsonp(res, data) { res.jsonp(data); } -function sendBodyXml(res, data) { +function sendBodyXml(res, data, method) { if (data === null) { res.header('Content-Length', '7'); res.send(''); } else if (data) { try { - var xml = toXML(data); + var xmlOptions = method.returns[0].xml || {}; + var xml = toXML(data, xmlOptions); res.send(xml); } catch (e) { res.status(500).send(e + '\n' + data); @@ -629,13 +651,24 @@ HttpContext.prototype.done = function(cb) { res.header('Content-Type', operationResults.contentType); } if (dataExists) { - operationResults.sendBody(res, data); + if (this.resultType !== 'file') { + operationResults.sendBody(res, data, method); + res.end(); + } else if (Buffer.isBuffer(data) || typeof(data) === 'string') { + res.end(data); + } else if (data.pipe) { + data.pipe(res); + } else { + var valueType = SharedMethod.getType(data); + var msg = 'Cannot create a file response from "' + valueType + '"'; + return cb(new Error(msg)); + } } else { if (res.statusCode === undefined || res.statusCode === 200) { res.statusCode = 204; } + res.end(); } - res.end(); cb(); }; diff --git a/lib/http-invocation.js b/lib/http-invocation.js index d4333994..f45c6d32 100644 --- a/lib/http-invocation.js +++ b/lib/http-invocation.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + /*! * Expose `HttpInvocation`. */ @@ -159,7 +164,8 @@ HttpInvocation.prototype._processArg = function(req, verb, query, accept) { HttpInvocation.prototype.createRequest = function() { var method = this.method; - var verb = method.getHttpMethod(); + var endpoint = method.getEndpoints()[0]; + var verb = endpoint.verb; var req = this.req = {method: verb || 'GET'}; var accepts = method.accepts; var ctorAccepts = null; @@ -168,7 +174,7 @@ HttpInvocation.prototype.createRequest = function() { var auth = this.auth; // initial url is the format - req.url = this.base + method.getFullPath(); + req.url = this.base + endpoint.fullPath; var parsedUrl = urlUtil.parse(req.url); diff --git a/lib/jsonrpc-adapter.js b/lib/jsonrpc-adapter.js index 986b7446..2b3bb512 100644 --- a/lib/jsonrpc-adapter.js +++ b/lib/jsonrpc-adapter.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + /*! * Expose `JsonRpcAdapter`. */ diff --git a/lib/remote-objects.js b/lib/remote-objects.js index cda747ce..7c4d2f78 100644 --- a/lib/remote-objects.js +++ b/lib/remote-objects.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + /*! * Expose `RemoteObjects`. */ @@ -10,13 +15,16 @@ module.exports = RemoteObjects; var EventEmitter = require('eventemitter2').EventEmitter2; var debug = require('debug')('strong-remoting:remotes'); +var deprecated = require('depd')('strong-remoting'); var util = require('util'); var urlUtil = require('url'); var inherits = util.inherits; var assert = require('assert'); var Dynamic = require('./dynamic'); var SharedClass = require('./shared-class'); +var SharedMethod = require('./shared-method'); var ExportsHelper = require('./exports-helper'); +var PhaseList = require('loopback-phase').PhaseList; // require the rest adapter for browserification // TODO(ritch) remove this somehow...? @@ -53,6 +61,8 @@ function RemoteObjects(options) { this.options = options || {}; this.exports = this.options.exports || {}; this._classes = {}; + + this._setupPhases(); } /*! @@ -381,6 +391,29 @@ RemoteObjects.prototype.afterError = function(methodMatch, fn) { this.on('afterError.' + methodMatch, fn); }; +RemoteObjects.prototype.registerPhaseHandler = function(phaseName, + methodNameWildcard, + handler) { + var pattern = methodNameWildcard.replace(/[.+?^${}()|[\]\\]/g, '\\$&') + // single star matches one segment only + .replace(/(^|\.)\*($|\.)/g, '$1[^.]*$2') + // double-start match one or more segments + .replace(/(^|\.)\*\*($|\.)/g, '$1.*$2'); + var matcher = new RegExp('^' + pattern + '$'); + + debug('registerPhaseHandler(%j) -> pattern %j', + methodNameWildcard, + pattern); + + this.phases.registerHandler(phaseName, function matchHandler(ctx, next) { + if (matcher.test(ctx.method.stringName)) { + handler(ctx, next); + } else { + next(); + } + }); +}; + /*! * Create a middleware style emit that supports wildcards. */ @@ -599,42 +632,64 @@ RemoteObjects.prototype._executeAuthorizationHook = function(ctx, cb) { } }; +RemoteObjects.prototype._setupPhases = function() { + var self = this; + self.phases = new PhaseList(); + var auth = self.phases.add('auth'); + var invoke = self.phases.add('invoke'); + + auth.use(function phaseAuthorization(ctx, next) { + self._executeAuthorizationHook(ctx, next); + }); + + invoke.before(function phaseBeforeInvoke(ctx, next) { + self.execHooks('before', ctx.method, ctx.getScope(), ctx, next); + }); + + invoke.use(function phaseInvoke(ctx, next) { + ctx.invoke(ctx.getScope(), ctx.method, function(err, result) { + if (!err) ctx.result = result; + next(err); + }); + }); + + invoke.after(function phaseAfterInvoke(ctx, next) { + self.execHooks('after', ctx.method, ctx.getScope(), ctx, next); + }); +}; + /** * Invoke the given shared method using the supplied context. * Execute registered before/after hooks. + * * @param {Object} ctx * @param {Object} method * @param {function(Error=)} cb */ RemoteObjects.prototype.invokeMethodInContext = function(ctx, method, cb) { var self = this; + var scope = ctx.getScope(); - var scope = this.getScope(ctx, method); - - self._executeAuthorizationHook(ctx, function(err) { - if (err) return triggerErrorAndCallBack(err); - - self.execHooks('before', method, scope, ctx, function(err) { - if (err) return triggerErrorAndCallBack(err); + if (cb === undefined && typeof method === 'function') { + // the new API with two arguments + cb = method; + method = ctx.method; + } else { + // backwards compatibility: invokeMethodInContext(ctx, method, cb) + // TODO remove in v3.0 + assert.equal(method, ctx.method); + deprecated('invokeMethodInContext(ctx, method, cb) is deprecated.' + + 'Pass the method as ctx.method instead.'); + } - ctx.invoke(scope, method, function(err, result) { - if (err) return triggerErrorAndCallBack(err); + self.phases.run(ctx, function interceptInvocationErrors(err) { + if (!err) return cb(); - ctx.result = result; - self.execHooks('after', method, scope, ctx, function(err) { - if (err) return triggerErrorAndCallBack(err); - cb(); - }); - }); - }); - }); - - function triggerErrorAndCallBack(err) { ctx.error = err; self.execHooks('afterError', method, scope, ctx, function(hookErr) { cb(hookErr || err); }); - } + }); }; /** @@ -644,11 +699,10 @@ RemoteObjects.prototype.invokeMethodInContext = function(ctx, method, cb) { */ RemoteObjects.prototype.getScope = function(ctx, method) { - // Static methods are invoked on the constructor (this = constructor fn) - // Prototype methods are invoked on the instance (this = instance) - return ctx.instance || - method.ctor || - method.sharedMethod && method.sharedMethod.ctor; + deprecated('remoteObjects.getScope(ctx, method) is deprecated, ' + + 'use ctx.getScope() instead'); + assert.equal(ctx.method, method); + return ctx.getScope(); }; /** diff --git a/lib/rest-adapter.js b/lib/rest-adapter.js index 88367017..21c623e3 100644 --- a/lib/rest-adapter.js +++ b/lib/rest-adapter.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + /*! * Expose `RestAdapter`. */ @@ -11,6 +16,7 @@ RestAdapter.RestMethod = RestMethod; * Module dependencies. */ +var deprecated = require('depd')('strong-remoting'); var EventEmitter = require('events').EventEmitter; var debug = require('debug')('strong-remoting:rest-adapter'); var util = require('util'); @@ -21,6 +27,7 @@ var bodyParser = require('body-parser'); var cors = require('cors'); var async = require('async'); var HttpInvocation = require('./http-invocation'); +var ContextBase = require('./context-base'); var HttpContext = require('./http-context'); var json = bodyParser.json; @@ -116,8 +123,9 @@ RestAdapter.prototype.invoke = function(method, ctorArgs, args, callback) { var invocation = new HttpInvocation( restMethod, ctorArgs, args, this.connection, remotes.auth ); - var ctx = { req: invocation.createRequest() }; - var scope = remotes.getScope(ctx, restMethod); + var ctx = new ContextBase(restMethod); + ctx.req = invocation.createRequest(); + var scope = ctx.getScope(); remotes.execHooks('before', restMethod, scope, ctx, function(err) { if (err) { return callback(err); } invocation.invoke(function(err) { @@ -368,22 +376,40 @@ RestAdapter.errorHandler = function(options) { res.statusCode = err.statusCode || err.status || 500; } - debug('Error in %s %s: %s', req.method, req.url, err.stack); - var data = { - name: err.name, - status: res.statusCode, - message: err.message || 'An unknown error occurred' - }; + if (Array.isArray(err)) { + var details = err.map(function(it) { + var data = generateResponseError(it); + delete data.statusCode; + return data; + }); - for (var prop in err) { - data[prop] = err[prop]; + var msg = 'Failed with multiple errors, see `details` for more information.'; + err = new Error(msg); + err.details = details; } - data.stack = err.stack; - if (process.env.NODE_ENV === 'production' || options.disableStackTrace) { - delete data.stack; + res.send({ error: generateResponseError(err) }); + + function generateResponseError(error) { + debug('Error in %s %s: %s', req.method, req.url, error.stack); + + var data = { + name: error.name, + status: res.statusCode, + message: error.message || 'An unknown error occurred', + }; + + for (var prop in error) { + data[prop] = error[prop]; + } + + data.stack = error.stack; + if (process.env.NODE_ENV === 'production' || options.disableStackTrace) { + delete data.stack; + } + + return data; } - res.send({ error: data }); } }; }; @@ -437,18 +463,18 @@ RestAdapter.prototype._invokeMethod = function(ctx, method, next) { if (method.rest.before) { steps.push(function invokeRestBefore(cb) { debug('Invoking rest.before for ' + ctx.methodString); - method.rest.before.call(remotes.getScope(ctx, method), ctx, cb); + method.rest.before.call(ctx.getScope(), ctx, cb); }); } steps.push( - this.remotes.invokeMethodInContext.bind(this.remotes, ctx, method) + this.remotes.invokeMethodInContext.bind(this.remotes, ctx) ); if (method.rest.after) { steps.push(function invokeRestAfter(cb) { debug('Invoking rest.after for ' + ctx.methodString); - method.rest.after.call(remotes.getScope(ctx, method), ctx, cb); + method.rest.after.call(ctx.getScope(), ctx, cb); }); } @@ -595,11 +621,30 @@ RestMethod.prototype.acceptsSingleBodyArgument = function() { getTypeString(accepts.type) == 'object' || false; }; +RestMethod.prototype.getEndpoints = function() { + var self = this; + return this.routes.map(function(route) { + var verbResult; + var verb = route.verb; + if (verb == 'all') { + verbResult = 'POST'; + } else if (verb == 'del') { + verbResult = 'DELETE'; + } else { + verbResult = verb.toUpperCase(); + } + return { + verb: verbResult, + fullPath: joinPaths(self.restClass.getPath(), route.path), + }; + }); +}; + RestMethod.prototype.getHttpMethod = function() { - var verb = this.routes[0].verb; - if (verb == 'all') return 'POST'; - if (verb == 'del') return 'DELETE'; - return verb.toUpperCase(); + // deprecate message to let the users know what they were using + // was retuning just the first route's verb + deprecated('getHttpMethod() is deprecated, use getEndpoints()[0].verb instead.'); + return this.getEndpoints()[0].verb; }; RestMethod.prototype.getPath = function() { @@ -607,7 +652,10 @@ RestMethod.prototype.getPath = function() { }; RestMethod.prototype.getFullPath = function() { - return joinPaths(this.restClass.getPath(), this.getPath()); + // deprecate message to let the users know what they were using + // was retuning just the first route's path + deprecated('getFullPath() is deprecated, use getEndpoints()[0].fullPath instead.'); + return this.getEndpoints()[0].fullPath; }; function getTypeString(ctorOrName) { diff --git a/lib/shared-class.js b/lib/shared-class.js index c03d1801..7f945a8d 100644 --- a/lib/shared-class.js +++ b/lib/shared-class.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + /*! * Expose `SharedClass`. */ diff --git a/lib/shared-method.js b/lib/shared-method.js index c473c0fa..95c8fcb1 100644 --- a/lib/shared-method.js +++ b/lib/shared-method.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + /*! * Expose `SharedMethod`. */ @@ -71,6 +76,10 @@ var assert = require('assert'); * either a single argument as an object or an ordered set of arguments as an array. * The `err` argument is assumed; do not specify. NOTE: Can have the same properties as * `accepts`, except for `http.target`. + * + * Additionally, one of the callback arguments can have `type: 'file'` and + * `root:true`, in which case this argument is sent in the raw form as + * a response body. Allowed values: `String`, `Buffer` or `ReadableStream` * @property {Boolean} [shared] Whether the method is shared. Default is `true`. * @property {Number} [status] The default status code. * @end @@ -384,6 +393,7 @@ function convertToBasicRemotingType(type) { case 'boolean': case 'buffer': case 'object': + case 'file': case 'any': return type; case 'array': @@ -520,9 +530,13 @@ SharedMethod.toResult = function(returns, raw, ctx) { return false; } + if (ctx && ctx.setReturnArgByName(item.name || item.arg, raw[index])) { + return false; + } + if (item.root) { - result = convert(raw[index]); - if (ctx) ctx.setReturnArgByName(item.name || item.arg, raw[index]); + var isFile = convertToBasicRemotingType(item.type) === 'file'; + result = isFile ? raw[index] : convert(raw[index]); return false; } @@ -530,10 +544,16 @@ SharedMethod.toResult = function(returns, raw, ctx) { }); returns.forEach(function(item, index) { - var value = convert(raw[index]); var name = item.name || item.arg; + if (convertToBasicRemotingType(item.type) === 'file') { + console.warn('%s: discarded non-root return argument %s of type "file"', + this.stringName, + name); + return; + } + + var value = convert(raw[index]); result[name] = value; - if (ctx) ctx.setReturnArgByName(name, value); }); return result; diff --git a/lib/socket-io-adapter.js b/lib/socket-io-adapter.js index 3e9bbfd8..e03e7935 100644 --- a/lib/socket-io-adapter.js +++ b/lib/socket-io-adapter.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + /** * Expose `SocketIOAdapter`. */ diff --git a/lib/socket-io-context.js b/lib/socket-io-context.js index c7e7cf00..66333444 100644 --- a/lib/socket-io-context.js +++ b/lib/socket-io-context.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + /** * Expose `SocketIOContext`. */ diff --git a/package.json b/package.json index d257e76d..48e5ec83 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "Remoting", "REST" ], - "version": "2.24.0", + "version": "2.26.0", "scripts": { "pretest": "grunt jscs jshint", "test": "mocha" @@ -17,11 +17,13 @@ "body-parser": "^1.12.4", "cors": "^2.6.0", "debug": "^2.2.0", + "depd": "^1.1.0", "eventemitter2": "^0.4.14", "express": "4.x", "inflection": "^1.7.1", "jayson": "^1.2.0", "js2xmlparser": "^0.1.9", + "loopback-phase": "^1.3.0", "mux-demux": "^3.7.9", "qs": "^2.4.2", "request": "^2.55.0", diff --git a/test/auth.test.js b/test/auth.test.js index c4f0ad25..0b765fb4 100644 --- a/test/auth.test.js +++ b/test/auth.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + var auth = require('http-auth'); var crypto = require('crypto'); var expect = require('chai').expect; diff --git a/test/authorize-hook.test.js b/test/authorize-hook.test.js index 5c0f4017..870a9fd3 100644 --- a/test/authorize-hook.test.js +++ b/test/authorize-hook.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + var expect = require('chai').expect; var express = require('express'); var RemoteObjects = require('../'); diff --git a/test/e2e/e2e-server.js b/test/e2e/e2e-server.js index e574e2a9..1ad680a8 100644 --- a/test/e2e/e2e-server.js +++ b/test/e2e/e2e-server.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + // this server should be started before running tests in the e2e directory var path = require('path'); var FIXTURES = path.join(__dirname, 'fixtures'); diff --git a/test/e2e/fixtures/remotes.js b/test/e2e/fixtures/remotes.js index fc5e8def..faaa05bc 100644 --- a/test/e2e/fixtures/remotes.js +++ b/test/e2e/fixtures/remotes.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + var RemoteObjects = require('../../../'); var remotes = module.exports = RemoteObjects.create(); diff --git a/test/e2e/fixtures/user.js b/test/e2e/fixtures/user.js index 37b7dfb5..8b0826fb 100644 --- a/test/e2e/fixtures/user.js +++ b/test/e2e/fixtures/user.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + var debug = require('debug')('test user'); module.exports = User; diff --git a/test/e2e/smoke.test.js b/test/e2e/smoke.test.js index 08c96afa..25d71272 100644 --- a/test/e2e/smoke.test.js +++ b/test/e2e/smoke.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + var RemoteObjects = require('../../'); var expect = require('chai').expect; var REMOTE_URL = 'http://localhost:3000'; diff --git a/test/helpers/shared-objects-factory.js b/test/helpers/shared-objects-factory.js index 07cc4540..ae70cb6b 100644 --- a/test/helpers/shared-objects-factory.js +++ b/test/helpers/shared-objects-factory.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + /* Copyright (c) 2013 StrongLoop, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/test/helpers/test-server.js b/test/helpers/test-server.js index 4a2e54d2..32dc79bd 100644 --- a/test/helpers/test-server.js +++ b/test/helpers/test-server.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + module.exports = function(callback) { }; diff --git a/test/http-context.test.js b/test/http-context.test.js index 303b7a7b..14bc67d8 100644 --- a/test/http-context.test.js +++ b/test/http-context.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + var request = require('supertest'); var HttpContext = require('../lib/http-context'); var SharedMethod = require('../lib/shared-method'); diff --git a/test/http-invocation.test.js b/test/http-invocation.test.js index fc4f6108..807b88d7 100644 --- a/test/http-invocation.test.js +++ b/test/http-invocation.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + var assert = require('assert'); var HttpInvocation = require('../lib/http-invocation'); var SharedMethod = require('../lib/shared-method'); diff --git a/test/jsonrpc.test.js b/test/jsonrpc.test.js index 3cb792b7..87a2417c 100644 --- a/test/jsonrpc.test.js +++ b/test/jsonrpc.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + var assert = require('assert'); var RemoteObjects = require('../'); var express = require('express'); diff --git a/test/karma.conf.js b/test/karma.conf.js index ae16d6a7..a48ce3b1 100644 --- a/test/karma.conf.js +++ b/test/karma.conf.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + // Karma configuration // http://karma-runner.github.io/0.12/config/configuration-file.html diff --git a/test/phase-handlers.test.js b/test/phase-handlers.test.js new file mode 100644 index 00000000..88bd81e9 --- /dev/null +++ b/test/phase-handlers.test.js @@ -0,0 +1,136 @@ +// Copyright IBM Corp. 2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + +var expect = require('chai').expect; +var express = require('express'); +var RemoteObjects = require('../'); +var User = require('./e2e/fixtures/user'); + +describe('phase handlers', function() { + var server, remotes, clientRemotes; + + beforeEach(function setupServer(done) { + var app = express(); + remotes = RemoteObjects.create(); + remotes.exports.User = User; + app.use(function(req, res, next) { + // always build a new handler to pick new methods added by tests + remotes.handler('rest')(req, res, next); + }); + server = app.listen(0, '127.0.0.1', done); + }); + + beforeEach(function setupClient() { + clientRemotes = RemoteObjects.create(); + clientRemotes.exports.User = User; + var url = 'http://127.0.0.1:' + server.address().port; + clientRemotes.connect(url, 'rest'); + }); + + afterEach(function teardownServer(done) { + server.close(done); + }); + + it('has built-in phases "auth" and "invoke"', function() { + expect(remotes.phases.getPhaseNames()).to.eql(['auth', 'invoke']); + }); + + it('invokes phases in the correct order', function(done) { + var phasesRun = []; + var pushNameAndNext = function(name) { + return function(ctx, next) { phasesRun.push(name); next(); }; + }; + + remotes.phases.find('auth').use(pushNameAndNext('phaseHandler-auth')); + var invokePhase = remotes.phases.find('invoke'); + invokePhase.before(pushNameAndNext('phaseHandler-invoke:before')); + invokePhase.use(pushNameAndNext('phaseHandler-invoke:use')); + invokePhase.after(pushNameAndNext('phaseHandler-invoke:after')); + + remotes.authorization = pushNameAndNext('hook-authorization'); + remotes.before('**', pushNameAndNext('hook-remotes.before')); + remotes.after('**', pushNameAndNext('hook-remotes.after')); + + User.pushName = function(cb) { phasesRun.push('invoke method'); cb(); }; + User.pushName.shared = true; + + invokeRemote('User.pushName', function(err) { + if (err) return done(err); + expect(phasesRun).to.eql([ + 'hook-authorization', + 'phaseHandler-auth', + 'hook-remotes.before', + 'phaseHandler-invoke:before', + 'invoke method', + 'phaseHandler-invoke:use', + 'hook-remotes.after', + 'phaseHandler-invoke:after', + ]); + done(); + }); + }); + + describe('registerPhaseHandler', function() { + var handlersRun; + + beforeEach(function() { + User.static = function(cb) { cb(); }; + User.static.shared = true; + + User.prototype.proto = function(cb) { cb(); }; + User.prototype.proto.shared = true; + + handlersRun = []; + function register(wildcard) { + remotes.registerPhaseHandler('invoke', wildcard, function(ctx, next) { + handlersRun.push(wildcard); + next(); + }); + } + + register('**'); + register('User.**'); + + register('User.*'); + register('User.static'); + register('User.proto'); // does not exist + + register('User.prototype.*'); + register('User.prototype.proto'); + register('User.prototype.static'); // does not exist + }); + + it('matches static methods using wildcards', function(done) { + invokeRemote('User.static', function(err) { + if (err) return done(err); + expect(handlersRun).to.eql([ + '**', + 'User.**', + 'User.*', + 'User.static', + ]); + done(); + }); + }); + + it('matches prototype methods using wildcards', function(done) { + invokeRemote('User.prototype.proto', function(err) { + if (err) return done(err); + expect(handlersRun).to.eql([ + '**', + 'User.**', + 'User.prototype.*', + 'User.prototype.proto', + ]); + done(); + }); + }); + }); + + function invokeRemote(method, callback) { + var args = []; + clientRemotes.invoke(method, args, callback); + } +}); diff --git a/test/rest-adapter.test.js b/test/rest-adapter.test.js index 24bac8f7..8ed49fe0 100644 --- a/test/rest-adapter.test.js +++ b/test/rest-adapter.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + var assert = require('assert'); var HttpInvocation = require('../lib/http-invocation'); var extend = require('util')._extend; @@ -8,6 +13,7 @@ var SharedClass = require('../lib/shared-class'); var SharedMethod = require('../lib/shared-method'); var expect = require('chai').expect; var factory = require('./helpers/shared-objects-factory.js'); +function NOOP() {} describe('RestAdapter', function() { var remotes; @@ -213,6 +219,8 @@ describe('RestAdapter', function() { }); describe('getHttpMethod', function() { + ignoreDeprecationsInThisBlock(); + it('returns POST for `all`', function() { var method = givenRestStaticMethod({ http: { verb: 'all'} }); expect(method.getHttpMethod()).to.equal('POST'); @@ -240,6 +248,8 @@ describe('RestAdapter', function() { }); describe('getFullPath', function() { + ignoreDeprecationsInThisBlock(); + it('returns class path + method path', function() { var method = givenRestStaticMethod( { http: { path: '/a-method' } }, @@ -250,6 +260,37 @@ describe('RestAdapter', function() { }); }); + describe('getEndpoints', function() { + it('should return verb and fullPath for multiple paths', function() { + var method = givenRestStaticMethod({ http: [ + { verb: 'DEL', path: '/testMethod1' }, + { verb: 'PUT', path: '/testMethod2' }, + ] }); + + var expectedEndpoints = [ + { + fullPath: '/testClass/testMethod1', + verb: 'DELETE', + }, { + fullPath: '/testClass/testMethod2', + verb: 'PUT', + }, + ]; + + expect(method.getEndpoints()).to.eql(expectedEndpoints); + }); + + it('should return verb and fullPath for single path', function() { + var method = givenRestStaticMethod({ http: { verb: 'all' }}); + expect(method.getEndpoints()).to.eql([ + { + verb: 'POST', + fullPath: '/testClass/testMethod', + }, + ]); + }); + }); + function givenRestStaticMethod(methodConfig, classConfig) { var name = 'testMethod'; methodConfig = extend({ shared: true }, methodConfig); @@ -439,3 +480,13 @@ describe('RestAdapter', function() { function someFunc() { } + +function ignoreDeprecationsInThisBlock() { + before(function() { + process.on('deprecation', NOOP); + }); + + after(function() { + process.removeListener('deprecation', NOOP); + }); +} diff --git a/test/rest.browser.test.js b/test/rest.browser.test.js index f9561757..33f77192 100644 --- a/test/rest.browser.test.js +++ b/test/rest.browser.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + var assert = require('assert'); var extend = require('util')._extend; var inherits = require('util').inherits; diff --git a/test/rest.test.js b/test/rest.test.js index d73986d6..41461575 100644 --- a/test/rest.test.js +++ b/test/rest.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + var assert = require('assert'); var extend = require('util')._extend; var inherits = require('util').inherits; @@ -7,6 +12,7 @@ var request = require('supertest'); var expect = require('chai').expect; var factory = require('./helpers/shared-objects-factory.js'); var Promise = global.Promise || require('bluebird'); +var Readable = require('stream').Readable; var ACCEPT_XML_OR_ANY = 'application/xml,*/*;q=0.8'; var TEST_ERROR = new Error('expected test error'); @@ -1359,6 +1365,121 @@ describe('strong-remoting-rest', function() { }); }); + it('should allow customized xml root element', function(done) { + var method = givenSharedStaticMethod( + function bar(cb) { + cb(null, {a: 1, b: 2}); + }, + { + returns: { + arg: 'data', type: 'object', root: true, + xml: { wrapperElement: 'foo'} + }, + http: { path: '/' } + } + ); + request(app).get(method.classUrl) + .set('Accept', 'application/xml') + .set('Content-Type', 'application/json') + .send() + .expect('Content-Type', /xml/) + .expect(200, function(err, res) { + expect(res.text).to.equal( + '\n' + + '\n ' + + '1\n ' + + '2\n' + + ''); + done(err, res); + }); + }); + + it('should allow xml declaration to be disabled', function(done) { + var method = givenSharedStaticMethod( + function bar(cb) { + cb(null, {a: 1, b: 2}); + }, + { + returns: { + arg: 'data', type: 'object', root: true, + xml: { declaration : false } + }, + http: { path: '/' } + } + ); + request(app).get(method.classUrl) + .set('Accept', 'application/xml') + .set('Content-Type', 'application/json') + .send() + .expect('Content-Type', /xml/) + .expect(200, function(err, res) { + expect(res.text).to.equal( + '\n ' + + '1\n ' + + '2\n' + + ''); + done(err, res); + }); + }); + + it('should allow string results to output as xml', function(done) { + var method = givenSharedStaticMethod( + function bar(cb) { + var stringResult = 'a quick brown fox jumps over the lazy dog'; + cb(null, stringResult); + }, + { + returns: { + root: true, + xml: { wrapperElement : 'text' } + }, + http: { path: '/' } + } + ); + request(app).get(method.classUrl) + .set('Accept', 'application/xml') + .set('Content-Type', 'application/json') + .send() + .expect('Content-Type', /xml/) + .expect(200, function(err, res) { + expect(res.text).to.equal( + '\n' + + 'a quick brown fox jumps over the lazy dog' + + ''); + done(err, res); + }); + }); + + it('should handle UTF-8 & special & reserved characters', function(done) { + var method = givenSharedStaticMethod( + function bar(cb) { + var stringA = 'foo\xC1\xE1\u0102\u03A9asd><=$~!@#$%^&*()-_=+/.,;\'"[]{}?'; + cb(null, {a: stringA}); + }, + { + returns: { + arg: 'data', type: 'object', root: true, + xml: { wrapperElement: false } + }, + http: { path: '/' } + } + ); + request(app).get(method.classUrl) + .set('Accept', 'application/xml') + .set('Content-Type', 'application/json') + .send() + .expect('Content-Type', /xml/) + .expect(200, function(err, res) { + expect(res).to.be.utf8; + expect(res.text).to.equal( + '\n' + + '\n ' + + 'fooÁáĂΩasd><=$~!@#$%^&*()-_=+/.,;'"[]{}?\n' + + ''); + done(); + }); + }); + it('should produce xml from json objects with toXML()', function(done) { var method = givenSharedStaticMethod( function bar(a, cb) { @@ -1461,6 +1582,70 @@ describe('strong-remoting-rest', function() { .end(expectErrorResponseContaining({message: 'an error'}, done)); }); + it('should return 500 if an array of errors is thrown', function(done) { + var testError = new Error('expected test error'); + var errArray = [testError, testError]; + + function method(error) { + return givenSharedStaticMethod(function(cb) { + cb(error); + }); + } + + request(app).get(method(testError).url) + .set('Accept', 'application/json') + .expect(500) + .end(function(err, res) { + if (err) return done(err); + var expectedDetail = res.body.error; + delete expectedDetail.statusCode; + + request(app).get(method(errArray).url) + .set('Accept', 'application/json') + .expect(500) + .end(function(err, res) { + if (err) return done(err); + var error = res.body.error; + expect(error).to.have.property('message').that.match(/multiple errors/); + expect(error).to.include.keys('details'); + expect(error.details).to.include(expectedDetail); + done(); + }); + }); + }); + + it('should return 500 if an array of errors is thrown', function(done) { + var testError = new Error('expected test error'); + var errArray = [testError, testError]; + + function method(error) { + return givenSharedStaticMethod(function(cb) { + cb(error); + }); + } + + request(app).get(method(testError).url) + .set('Accept', 'application/json') + .expect(500) + .end(function(err, res) { + if (err) return done(err); + var expectedDetail = res.body.error; + delete expectedDetail.statusCode; + + request(app).get(method(errArray).url) + .set('Accept', 'application/json') + .expect(500) + .end(function(err, res) { + if (err) return done(err); + var error = res.body.error; + expect(error).to.have.property('message').that.match(/multiple errors/); + expect(error).to.include.keys('details'); + expect(error.details).to.include(expectedDetail); + done(); + }); + }); + }); + it('should return 500 if an error string is thrown', function(done) { remotes.shouldThrow = { bar: function(fn) { @@ -1931,20 +2116,47 @@ describe('strong-remoting-rest', function() { describe('result args as headers', function() { it('sets the header using the callback arg', function(done) { - var val = 'foobar'; + var A_STRING_VALUE = 'foobar'; var method = givenSharedStaticMethod( function fn(input, cb) { - cb(null, input); + cb(null, input, input); }, { accepts: {arg: 'input', type: 'string'}, - returns: {arg: 'output', type: 'string', http: { target: 'header' } } + returns: [ + {arg: 'value', type: 'string' }, + {arg: 'output', type: 'string', http: { target: 'header' } } + ] } ); - json(method.url + '?input=' + val) - .expect('output', val) - .expect(200, done); + json(method.url + '?input=' + A_STRING_VALUE) + .expect(200) + .expect('output', A_STRING_VALUE) + .expect({ value: A_STRING_VALUE }) + .end(done); }); + + it('sets the header using the callback arg - root arg', function(done) { + var A_STRING_VALUE = 'foobar'; + var method = givenSharedStaticMethod( + function fn(input, cb) { + cb(null, { value: input }, input); + }, + { + accepts: {arg: 'input', type: 'string'}, + returns: [ + {arg: 'value', type: 'object', root: true }, + {arg: 'output', type: 'string', http: { target: 'header' } } + ] + } + ); + json(method.url + '?input=' + A_STRING_VALUE) + .expect(200) + .expect('output', A_STRING_VALUE) + .expect({ value: A_STRING_VALUE }) + .end(done); + }); + it('sets the custom header using the callback arg', function(done) { var val = 'foobar'; var method = givenSharedStaticMethod( @@ -1966,6 +2178,72 @@ describe('strong-remoting-rest', function() { }); }); + describe('returns type "file"', function() { + var METHOD_SIGNATURE = { + returns: [ + { arg: 'body', type: 'file', root: true }, + { arg: 'Content-Type', type: 'string', http: { target: 'header' } }, + ], + }; + + it('should send back Buffer body', function(done) { + var method = givenSharedStaticMethod( + function(cb) { cb(null, new Buffer('some-text'), 'text/plain'); }, + METHOD_SIGNATURE); + + return request(app).get(method.url) + .expect(200) + .expect('Content-Type', /^text\/plain/) + .expect('some-text') + .end(done); + }); + + it('should send back String body', function(done) { + var method = givenSharedStaticMethod( + function(cb) { cb(null, 'some-text', 'text/plain'); }, + METHOD_SIGNATURE); + + return request(app).get(method.url) + .expect(200) + .expect('Content-Type', /^text\/plain/) + .expect('some-text') + .end(done); + }); + + it('should send back Stream body', function(done) { + var method = givenSharedStaticMethod( + function(cb) { + var stream = new Readable(); + stream.push('some-text'); + stream.push(null); // EOF + cb(null, stream, 'text/plain'); + }, + METHOD_SIGNATURE); + + return request(app).get(method.url) + .expect(200) + .expect('Content-Type', /^text\/plain/) + .expect('some-text') + .end(done); + }); + + it('should fail for unsupported value type', function(done) { + var method = givenSharedStaticMethod( + function(cb) { cb(null, [1, 2]); }, + METHOD_SIGNATURE); + + return request(app).get(method.url) + .expect(500) + .expect('Content-Type', /json/) + .end(function(err, res) { + if (err) return done(err); + expect(res.body).to.have.property('error'); + expect(res.body.error.message).to.match(/array/); + done(); + }); + }); + }); + it('returns correct error response body', function(done) { function TestError() { Error.captureStackTrace(this, TestError); @@ -2615,6 +2893,24 @@ describe('strong-remoting-rest', function() { }); }); + it('is not called on success', function(done) { + var hookCalled = false; + var method = givenSharedStaticMethod(function(cb) { + cb(); + }); + + objects.afterError(method.name, function(ctx, next) { + hookCalled = true; + next(); + }); + + json(method.url).end(function(err) { + if (err) return done(err); + expect(hookCalled, 'hookCalled').to.equal(false); + done(); + }); + }); + function verifyErrorHookIsCalled(method, expectedError, done) { var hookContext = 'hook not called'; diff --git a/test/shared-class.test.js b/test/shared-class.test.js index 3bd48426..d659a9fa 100644 --- a/test/shared-class.test.js +++ b/test/shared-class.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + var assert = require('assert'); var extend = require('util')._extend; var expect = require('chai').expect; diff --git a/test/shared-method.test.js b/test/shared-method.test.js index 52c3199f..0527a62f 100644 --- a/test/shared-method.test.js +++ b/test/shared-method.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + var assert = require('assert'); var extend = require('util')._extend; var expect = require('chai').expect; diff --git a/test/streams.js b/test/streams.js index 41bc4180..e2f7e534 100644 --- a/test/streams.js +++ b/test/streams.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + var assert = require('assert'); var RemoteObjects = require('../'); var expect = require('chai').expect; @@ -32,24 +37,33 @@ describe('strong-remoting', function() { .expect('Content-Type', /json/); } - it('should stream the file output', function(done) { + function createSteam() { remotes.fs = fs; fs.createReadStream.shared = true; fs.createReadStream.accepts = [ { arg: 'path', type: 'string' }, - { arg: 'encoding', type: 'string' } + { arg: 'encoding', type: 'string' }, ]; - fs.createReadStream.returns = {arg: 'res', type: 'stream'}; + fs.createReadStream.returns = { arg: 'res', type: 'stream' }; fs.createReadStream.http = { verb: 'get', - // path: '/fs/createReadStream', pipe: { - dest: 'res' - } + dest: 'res', + }, }; + } + it('should stream the file output', function(done) { + createSteam(); json('get', '/fs/createReadStream?path=' + __dirname + '/data/foo.json&encoding=utf8') - .expect({bar: 'baz'}, done); + .expect({ bar: 'baz' }, done); + }); + + it('should stream the file output with no compression', function(done) { + createSteam(); + request(app).get('/fs/createReadStream') + .expect('Content-Encoding', 'x-no-compression'); + done(); }); }); diff --git a/test/type.test.js b/test/type.test.js index e9486260..0e7d8573 100644 --- a/test/type.test.js +++ b/test/type.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: strong-remoting +// This file is licensed under the Artistic License 2.0. +// License text available at https://opensource.org/licenses/Artistic-2.0 + var assert = require('assert'); var Dynamic = require('../lib/dynamic'); var RemoteObjects = require('../');