From 5671bf14400be47b9e722a1ebcf7932c85e6890c Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Tue, 8 Jul 2014 16:34:06 +0200 Subject: [PATCH] Implement joi validation of query and path params. Fixes #936. --- js/server/modules/org/arangodb/actions.js | 190 +++++++++--------- .../modules/org/arangodb/foxx/controller.js | 17 +- .../modules/org/arangodb/foxx/internals.js | 32 ++- .../org/arangodb/foxx/request_context.js | 107 +++++++--- 4 files changed, 213 insertions(+), 133 deletions(-) diff --git a/js/server/modules/org/arangodb/actions.js b/js/server/modules/org/arangodb/actions.js index 574a4aabaf..5fa2133d8e 100644 --- a/js/server/modules/org/arangodb/actions.js +++ b/js/server/modules/org/arangodb/actions.js @@ -61,7 +61,7 @@ var ALL_METHODS = [ "DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT", "PATCH" ] //////////////////////////////////////////////////////////////////////////////// /// @brief function that's returned for non-implemented actions //////////////////////////////////////////////////////////////////////////////// - + function notImplementedFunction (route, message) { 'use strict'; @@ -259,7 +259,7 @@ function lookupCallbackActionCallbackString (route, action, context) { catch (err) { func = errorFunction( route, - "an error occurred while loading function '" + "an error occurred while loading function '" + action.callback + "': " + String(err.stack || err)); } @@ -292,7 +292,7 @@ function lookupCallbackActionCallback (route, action, context) { return { controller: errorFunction( route, - "an error occurred while loading function '" + "an error occurred while loading function '" + action.callback + "': unknown type '" + String(typeof action.callback) + "'"), options: action.options || {}, methods: action.methods || ALL_METHODS @@ -331,22 +331,22 @@ function lookupCallbackActionDo (route, action) { catch (err) { if (err.hasOwnProperty("moduleNotFound") && err.moduleNotFound) { func = notImplementedFunction( - route, - "an error occurred while loading action named '" + name + route, + "an error occurred while loading action named '" + name + "' in module '" + joined + "': " + String(err.stack || err)); } else { func = errorFunction( route, - "an error occurred while loading action named '" + name + "an error occurred while loading action named '" + name + "' in module '" + joined + "': " + String(err.stack || err)); } } if (func === null || typeof func !== 'function') { func = errorFunction( - route, - "invalid definition for the action named '" + name + route, + "invalid definition for the action named '" + name + "' in module '" + joined + "'"); } @@ -366,7 +366,7 @@ function lookupCallbackActionController (route, action) { var func; var mdl; - var httpMethods = { + var httpMethods = { 'get': exports.GET, 'head': exports.HEAD, 'put': exports.PUT, @@ -389,12 +389,12 @@ function lookupCallbackActionController (route, action) { if (req.requestType === httpMethods[m] && mdl.hasOwnProperty(m)) { func = mdl[m] || errorFunction( - route, - "invalid definition for " + m - + " action in action controller module '" + route, + "invalid definition for " + m + + " action in action controller module '" + action.controller + "'"); - return func(req, res, options, next); + return func(req, res, options, next); } } } @@ -418,14 +418,14 @@ function lookupCallbackActionController (route, action) { catch (err) { if (err.hasOwnProperty("moduleNotFound") && err.moduleNotFound) { return notImplementedFunction( - route, - "cannot load/execute action controller module '" + route, + "cannot load/execute action controller module '" + action.controller + ": " + String(err.stack || err)); } return errorFunction( - route, - "cannot load/execute action controller module '" + route, + "cannot load/execute action controller module '" + action.controller + ": " + String(err.stack || err)); } } @@ -438,7 +438,7 @@ function lookupCallbackActionPrefixController (route, action) { 'use strict'; var prefixController = action.prefixController; - var httpMethods = { + var httpMethods = { 'get': exports.GET, 'head': exports.HEAD, 'put': exports.PUT, @@ -496,11 +496,11 @@ function lookupCallbackActionPrefixController (route, action) { } } } - + if (mdl.hasOwnProperty('do')) { func = mdl['do'] || errorFunction( - route, + route, "Invalid definition for do action in prefix controller '" + action.prefixController + "'"); @@ -509,7 +509,7 @@ function lookupCallbackActionPrefixController (route, action) { } catch (err2) { return errorFunction( - route, + route, "Cannot load/execute prefix controller '" + action.prefixController + "': " + String(err2.stack || err2))( req, res, options, next); @@ -592,7 +592,7 @@ function lookupCallback (route, context) { } //////////////////////////////////////////////////////////////////////////////// -/// @brief intersect methods +/// @brief intersect methods //////////////////////////////////////////////////////////////////////////////// function intersectMethods (a, b) { @@ -611,7 +611,7 @@ function intersectMethods (a, b) { } for (j = 0; j < a.length; j++) { - var name = a[j].toUpperCase(); + var name = a[j].toUpperCase(); if (d[name]) { results.push(name); @@ -697,7 +697,7 @@ function defineRoutePart (route, subwhere, parts, pos, constraint, callback) { for (i = 0; i < part.parameters.length; ++i) { p = part.parameters[i]; subsub = { parameter: p, match: {} }; - subwhere.parameters.push(subsub); + subwhere.parameters.push(subsub); if (constraint.hasOwnProperty(p)) { subsub.constraint = constraint[p]; @@ -858,13 +858,13 @@ function flattenRouting (routes, path, urlParameters, depth, prefix) { for (i = 0; i < sorted.length; ++i) { sorted[i] = { - path: path, + path: path, regexp: new RegExp("^" + path + "$"), prefix: prefix, depth: depth, urlParameters: urlParameters, callback: sorted[i].callback, - route: sorted[i].route + route: sorted[i].route }; } @@ -905,7 +905,7 @@ function routeRequest (req, res) { execute = function () { if (action.route === undefined) { - exports.resultNotFound(req, res, arangodb.ERROR_HTTP_NOT_FOUND, + exports.resultNotFound(req, res, arangodb.ERROR_HTTP_NOT_FOUND, "unknown path '" + path + "'"); return; } @@ -942,7 +942,7 @@ function routeRequest (req, res) { if (func === null || typeof func !== 'function') { func = exports.errorFunction(action.route, - 'Invalid callback definition found for route ' + 'Invalid callback definition found for route ' + JSON.stringify(action.route)); } @@ -1125,7 +1125,7 @@ function getJsonBody (req, res, code) { /// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// -function resultError (req, res, httpReturnCode, errorNum, errorMessage, headers, keyvals) { +function resultError (req, res, httpReturnCode, errorNum, errorMessage, headers, keyvals) { 'use strict'; var i; @@ -1140,7 +1140,7 @@ function resultError (req, res, httpReturnCode, errorNum, errorMessage, headers, errorMessage = errorNum; errorNum = httpReturnCode; } - + if (errorMessage === undefined || errorMessage === null) { msg = getErrorMessage(errorNum); } @@ -1162,11 +1162,11 @@ function resultError (req, res, httpReturnCode, errorNum, errorMessage, headers, result.code = httpReturnCode; result.errorNum = errorNum; result.errorMessage = msg; - + res.body = JSON.stringify(result); if (headers !== undefined && headers !== null) { - res.headers = headers; + res.headers = headers; } } @@ -1184,10 +1184,10 @@ function reloadRouting () { // ............................................................................. // clear the routing cache // ............................................................................. - - // work with a local variable first + + // work with a local variable first var routingCache = { }; - + routingCache.flat = {}; routingCache.routes = {}; routingCache.middleware = {}; @@ -1250,7 +1250,7 @@ function reloadRouting () { defineRoute(route, storage, url, callback); }; - + // ............................................................................. // analyses a new route // ............................................................................. @@ -1290,10 +1290,10 @@ function reloadRouting () { for (i = 0; i < r.length; ++i) { var context = { appModule: appModule }; - installRoute(routingCache[key], - urlPrefix, - modulePrefix, - context, + installRoute(routingCache[key], + urlPrefix, + modulePrefix, + context, r[i]); } } @@ -1341,7 +1341,7 @@ function reloadRouting () { routingCache.flat[method] = b.concat(a); } - + // once we're done, we can set the complete routing cache for database // doing this here instead of above ensures we don't have a half-updated routing // cache if this function fails somewhere in the middle @@ -1405,11 +1405,11 @@ function firstRouting (type, parts) { 'use strict'; var url = parts; - + if (undefined === RoutingCache[arangodb.db._name()]) { reloadRouting(); } - + var routingCache = RoutingCache[arangodb.db._name()]; if (typeof url === 'string') { @@ -1452,7 +1452,7 @@ function firstRouting (type, parts) { function badParameter (req, res, name) { 'use strict'; - resultError(req, res, exports.HTTP_BAD, exports.HTTP_BAD, + resultError(req, res, exports.HTTP_BAD, exports.HTTP_BAD, "invalid value for parameter '" + name + "'"); } @@ -1469,24 +1469,24 @@ function badParameter (req, res, name) { /// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// -function resultOk (req, res, httpReturnCode, result, headers) { +function resultOk (req, res, httpReturnCode, result, headers) { 'use strict'; res.responseCode = httpReturnCode; res.contentType = "application/json; charset=utf-8"; - + // add some default attributes to result if (result === undefined) { result = {}; } - result.error = false; + result.error = false; result.code = httpReturnCode; - + res.body = JSON.stringify(result); - + if (headers !== undefined) { - res.headers = headers; + res.headers = headers; } } @@ -1532,7 +1532,7 @@ function resultNotFound (req, res, code, msg, headers) { function resultNotImplemented (req, res, msg, headers) { 'use strict'; - resultError(req, + resultError(req, res, exports.HTTP_NOT_IMPLEMENTED, arangodb.ERROR_NOT_IMPLEMENTED, @@ -1556,11 +1556,11 @@ function resultUnsupported (req, res, headers) { exports.HTTP_METHOD_NOT_ALLOWED, arangodb.ERROR_HTTP_METHOD_NOT_ALLOWED, "Unsupported method", - headers); + headers); } //////////////////////////////////////////////////////////////////////////////// -/// @brief internal function for handling redirects +/// @brief internal function for handling redirects //////////////////////////////////////////////////////////////////////////////// function handleRedirect (req, res, options, headers) { @@ -1582,7 +1582,7 @@ function handleRedirect (req, res, options, headers) { else { url += req.server.address + ":" + req.server.port; } - + if (options.relative) { var u = req.url; @@ -1637,7 +1637,7 @@ function resultPermanentRedirect (req, res, options, headers) { } //////////////////////////////////////////////////////////////////////////////// -/// @startDocuBlock actionsResultTemporaryRedirect +/// @startDocuBlock actionsResultTemporaryRedirect /// /// `actions.resultTemporaryRedirect(req, res, options, headers)` /// @@ -1649,7 +1649,7 @@ function resultTemporaryRedirect (req, res, options, headers) { 'use strict'; res.responseCode = exports.HTTP_TEMPORARY_REDIRECT; - + handleRedirect(req, res, options, headers); } @@ -1693,7 +1693,7 @@ function resultCursor (req, res, cursor, code, options) { if (hasNext) { cursor.persist(); - cursorId = cursor.id(); + cursorId = cursor.id(); } else { cursorId = null; @@ -1713,7 +1713,7 @@ function resultCursor (req, res, cursor, code, options) { // do not use cursor after this - var result = { + var result = { result : rows, hasMore : hasNext }; @@ -1721,7 +1721,7 @@ function resultCursor (req, res, cursor, code, options) { if (cursorId) { result.id = cursorId; } - + if (hasCount) { result.count = count; } @@ -1798,8 +1798,8 @@ function indexNotFound (req, res, collection, index, headers) { /// /// `actions.resultException(req, res, err, headers, verbose)` /// -/// The function generates an error response. If @FA{verbose} is set to -/// *true* or not specified (the default), then the error stack trace will +/// The function generates an error response. If @FA{verbose} is set to +/// *true* or not specified (the default), then the error stack trace will /// be included in the error message if available. /// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// @@ -1827,44 +1827,44 @@ function resultException (req, res, err, headers, verbose) { } if (err.errorMessage !== "" && ! verbose) { - msg = err.errorMessage; + msg = err.errorMessage; } switch (num) { - case arangodb.ERROR_INTERNAL: - case arangodb.ERROR_OUT_OF_MEMORY: - case arangodb.ERROR_GRAPH_TOO_MANY_ITERATIONS: - code = exports.HTTP_SERVER_ERROR; + case arangodb.ERROR_INTERNAL: + case arangodb.ERROR_OUT_OF_MEMORY: + case arangodb.ERROR_GRAPH_TOO_MANY_ITERATIONS: + code = exports.HTTP_SERVER_ERROR; break; - - case arangodb.ERROR_FORBIDDEN: - case arangodb.ERROR_ARANGO_USE_SYSTEM_DATABASE: - code = exports.HTTP_FORBIDDEN; + + case arangodb.ERROR_FORBIDDEN: + case arangodb.ERROR_ARANGO_USE_SYSTEM_DATABASE: + code = exports.HTTP_FORBIDDEN; break; - - case arangodb.ERROR_ARANGO_COLLECTION_NOT_FOUND: - case arangodb.ERROR_ARANGO_DOCUMENT_NOT_FOUND: - case arangodb.ERROR_ARANGO_DATABASE_NOT_FOUND: - case arangodb.ERROR_ARANGO_ENDPOINT_NOT_FOUND: - case arangodb.ERROR_ARANGO_NO_INDEX: - case arangodb.ERROR_ARANGO_INDEX_NOT_FOUND: - case arangodb.ERROR_CURSOR_NOT_FOUND: - case arangodb.ERROR_USER_NOT_FOUND: + + case arangodb.ERROR_ARANGO_COLLECTION_NOT_FOUND: + case arangodb.ERROR_ARANGO_DOCUMENT_NOT_FOUND: + case arangodb.ERROR_ARANGO_DATABASE_NOT_FOUND: + case arangodb.ERROR_ARANGO_ENDPOINT_NOT_FOUND: + case arangodb.ERROR_ARANGO_NO_INDEX: + case arangodb.ERROR_ARANGO_INDEX_NOT_FOUND: + case arangodb.ERROR_CURSOR_NOT_FOUND: + case arangodb.ERROR_USER_NOT_FOUND: code = exports.HTTP_NOT_FOUND; - break; - - case arangodb.ERROR_REQUEST_CANCELED: - code = exports.HTTP_REQUEST_TIMEOUT; - break; - - case arangodb.ERROR_ARANGO_DUPLICATE_NAME: - case arangodb.ERROR_ARANGO_DUPLICATE_IDENTIFIER: - code = exports.HTTP_CONFLICT; break; - case arangodb.ERROR_CLUSTER_UNSUPPORTED: + case arangodb.ERROR_REQUEST_CANCELED: + code = exports.HTTP_REQUEST_TIMEOUT; + break; + + case arangodb.ERROR_ARANGO_DUPLICATE_NAME: + case arangodb.ERROR_ARANGO_DUPLICATE_IDENTIFIER: + code = exports.HTTP_CONFLICT; + break; + + case arangodb.ERROR_CLUSTER_UNSUPPORTED: code = exports.HTTP_NOT_IMPLEMENTED; - break; + break; } } else if (err instanceof TypeError) { @@ -1875,7 +1875,7 @@ function resultException (req, res, err, headers, verbose) { num = arangodb.ERROR_HTTP_SERVER_ERROR; code = exports.HTTP_SERVER_ERROR; } - + resultError(req, res, code, num, msg, headers); } @@ -2052,12 +2052,12 @@ function addCookie (res, name, value, lifeTime, path, domain, secure, httpOnly) if (value === undefined) { return; } - + var cookie = { 'name' : name, 'value' : value }; - + if (lifeTime !== undefined && lifeTime !== null) { cookie.lifeTime = parseInt(lifeTime, 10); } @@ -2073,11 +2073,11 @@ function addCookie (res, name, value, lifeTime, path, domain, secure, httpOnly) if (httpOnly !== undefined && httpOnly !== null) { cookie.httpOnly = (httpOnly) ? true : false; } - + if (res.cookies === undefined || res.cookies === null) { res.cookies = []; } - + res.cookies.push(cookie); } diff --git a/js/server/modules/org/arangodb/foxx/controller.js b/js/server/modules/org/arangodb/foxx/controller.js index 150571de68..45e1ab8ef3 100644 --- a/js/server/modules/org/arangodb/foxx/controller.js +++ b/js/server/modules/org/arangodb/foxx/controller.js @@ -156,8 +156,9 @@ extend(Controller.prototype, { handleRequest: function (method, route, callback) { 'use strict'; - var newRoute = internal.constructRoute(method, route, callback, this), - requestContext = new RequestContext(this.allRoutes, this.models, newRoute, this.rootElement), + var constraints = {queryParams: {}, urlParams: {}}, + newRoute = internal.constructRoute(method, route, callback, this, constraints), + requestContext = new RequestContext(this.allRoutes, this.models, newRoute, this.rootElement, constraints), summary, undocumentedBody; @@ -482,7 +483,7 @@ extend(Controller.prototype, { /// work. This includes sending a response to the user. This defaults to a function /// that sets the response to 401 and returns a JSON with *error* set to /// "Username or Password was wrong". -/// +/// /// Both *onSuccess* and *onError* should take request and result as arguments. /// /// @EXAMPLES @@ -510,7 +511,7 @@ extend(Controller.prototype, { /// This works pretty similar to the logout function and adds a path to your /// app for the logout functionality. You can customize it with a custom *onSuccess* /// and *onError* function: -/// +/// /// * *onSuccess* is a function that you can define to do something if the logout was /// successful. This includes sending a response to the user. This defaults to a /// function that returns a JSON with *message* set to "logged out". @@ -518,7 +519,7 @@ extend(Controller.prototype, { /// work. This includes sending a response to the user. This defaults to a function /// that sets the response to 401 and returns a JSON with *error* set to /// "No session was found". -/// +/// /// Both *onSuccess* and *onError* should take request and result as arguments. /// /// @@ -555,9 +556,9 @@ extend(Controller.prototype, { /// work. This includes sending a response to the user. This defaults to a function /// that sets the response to 401 and returns a JSON with *error* set to /// "Registration failed". -/// +/// /// Both *onSuccess* and *onError* should take request and result as arguments. -/// +/// /// You can also set the fields containing the username and password via *usernameField* /// (defaults to *username*) and *passwordField* (defaults to *password*). /// If you want to accept additional attributes for the user document, use the option @@ -608,7 +609,7 @@ extend(Controller.prototype, { /// work. This includes sending a response to the user. This defaults to a function /// that sets the response to 401 and returns a JSON with *error* set to /// "No session was found". -/// +/// /// Both *onSuccess* and *onError* should take request and result as arguments. /// /// @EXAMPLES diff --git a/js/server/modules/org/arangodb/foxx/internals.js b/js/server/modules/org/arangodb/foxx/internals.js index 84640174f8..9ce91856a1 100644 --- a/js/server/modules/org/arangodb/foxx/internals.js +++ b/js/server/modules/org/arangodb/foxx/internals.js @@ -28,7 +28,9 @@ /// @author Copyright 2013, triAGENS GmbH, Cologne, Germany //////////////////////////////////////////////////////////////////////////////// -var is = require("org/arangodb/is"), +var _ = require("underscore"), + is = require("org/arangodb/is"), + actions = require("org/arangodb/actions"), constructUrlObject, constructNickname, constructRoute, @@ -84,17 +86,37 @@ constructNickname = function (httpMethod, url) { .toLowerCase(); }; -constructRoute = function (method, route, callback, controller) { +constructRoute = function (method, route, callback, controller, constraints) { 'use strict'; return { url: constructUrlObject(route, undefined, method), action: { callback: function (req, res) { - Object.keys(controller.injectors).forEach(function (key) { - if (Object.prototype.hasOwnProperty.call(controller.injected, key)) { + if (constraints) { + try { + _.each({ + urlParameters: constraints.urlParams, + parameters: constraints.queryParams + }, function (paramConstraints, paramsPropertyName) { + var params = req[paramsPropertyName]; + _.each(paramConstraints, function (constraint, paramName) { + var result = constraint.validate(params[paramName]); + params[paramName] = result.value; + if (result.error) { + result.error.message = 'Invalid value for "' + paramName + '": ' + result.error.message; + throw result.error; + } + }); + }); + } catch (err) { + actions.resultBad(req, res, actions.HTTP_BAD, err.message); + return; + } + } + _.each(controller.injectors, function (injector, key) { + if (_.has(controller.injected, key)) { return; } - var injector = controller.injectors[key]; if (typeof injector === 'function') { controller.injected[key] = injector(); } else { diff --git a/js/server/modules/org/arangodb/foxx/request_context.js b/js/server/modules/org/arangodb/foxx/request_context.js index e224d3d978..6d1dd1923d 100644 --- a/js/server/modules/org/arangodb/foxx/request_context.js +++ b/js/server/modules/org/arangodb/foxx/request_context.js @@ -180,7 +180,7 @@ extend(SwaggerDocs.prototype, { /// Used for documenting and constraining the routes. //////////////////////////////////////////////////////////////////////////////// -RequestContext = function (executionBuffer, models, route, rootElement) { +RequestContext = function (executionBuffer, models, route, rootElement, constraints) { 'use strict'; this.route = route; this.typeToRegex = { @@ -189,6 +189,7 @@ RequestContext = function (executionBuffer, models, route, rootElement) { "string": "/[^/]+/" }; this.rootElement = rootElement; + this.constraints = constraints; this.docs = new SwaggerDocs(this.route.docs, models); this.docs.addNickname(route.docs.httpMethod, route.url.match); @@ -202,11 +203,9 @@ extend(RequestContext.prototype, { /// @startDocuBlock JSF_foxx_RequestContext_pathParam /// /// If you defined a route "/foxx/:id", you can constrain which format a path -/// parameter (*/foxx/12*) can have by giving it a type. We currently support -/// the following types: +/// parameter (*/foxx?a=12*) can have by giving it a *joi* type. /// -/// * int -/// * string +/// For more information on *joi* see [the official Joi documentation](https://github.com/spumko/joi). /// /// You can also provide a description of this parameter. /// @@ -216,8 +215,7 @@ extend(RequestContext.prototype, { /// app.get("/foxx/:id", function { /// // Do something /// }).pathParam("id", { -/// description: "Id of the Foxx", -/// type: "int" +/// type: joi.number().integer().required().description("Id of the Foxx") /// }); /// ``` /// @endDocuBlock @@ -226,14 +224,48 @@ extend(RequestContext.prototype, { pathParam: function (paramName, attributes) { 'use strict'; var url = this.route.url, - constraint = url.constraint || {}; + urlConstraint = url.constraint || {}, + type = attributes.type, + required = attributes.required, + description = attributes.description, + constraint = type, + regexType = type, + cfg; - constraint[paramName] = this.typeToRegex[attributes.type]; - if (!constraint[paramName]) { - throw new Error("Illegal attribute type: " + attributes.type); + if (type && typeof type.describe === 'function') { + if (typeof required === 'boolean') { + constraint = required ? constraint.required() : constraint.optional(); + } + if (typeof description === 'string') { + constraint = constraint.description(description); + } + this.constraints.urlParams[paramName] = constraint; + cfg = constraint.describe(); + required = Boolean(cfg.flags && cfg.flags.presense === 'required'); + description = cfg.description; + type = cfg.type; + if ( + type === 'number' && + _.isArray(cfg.rules) && + _.some(cfg.rules, function (rule) { + return rule.name === 'integer'; + }) + ) { + type = 'integer'; + } + if (_.has(this.typeToRegex, type)) { + regexType = type; + } else { + regexType = 'string'; + } } - this.route.url = internal.constructUrlObject(url.match, constraint, url.methods[0]); - this.docs.addPathParam(paramName, attributes.description, attributes.type); + + urlConstraint[paramName] = this.typeToRegex[regexType]; + if (!urlConstraint[paramName]) { + throw new Error("Illegal attribute type: " + regexType); + } + this.route.url = internal.constructUrlObject(url.match, urlConstraint, url.methods[0]); + this.docs.addPathParam(paramName, description, type); return this; }, @@ -245,14 +277,12 @@ extend(RequestContext.prototype, { /// Describe a query parameter: /// /// If you defined a route "/foxx", you can constrain which format a query -/// parameter (*/foxx?a=12*) can have by giving it a type. We currently support -/// the following types: +/// parameter (*/foxx?a=12*) can have by giving it a *joi* type. /// -/// * int -/// * string +/// For more information on *joi* see [the official Joi documentation](https://github.com/spumko/joi). /// -/// You can also provide a description of this parameter, if it is required and -/// if you can provide the parameter multiple times. +/// You can also provide a description of this parameter and +/// whether you can provide the parameter multiple times. /// /// @EXAMPLES /// @@ -260,9 +290,7 @@ extend(RequestContext.prototype, { /// app.get("/foxx", function { /// // Do something /// }).queryParam("id", { -/// description: "Id of the Foxx", -/// type: "int", -/// required: true, +/// type: joi.number().integer().required().description("Id of the Foxx"), /// allowMultiple: false /// }); /// ``` @@ -271,11 +299,40 @@ extend(RequestContext.prototype, { queryParam: function (paramName, attributes) { 'use strict'; + var type = attributes.type, + required = attributes.required, + description = attributes.description, + constraint = type, + cfg; + + if (type && typeof type.describe === 'function') { + if (typeof required === 'boolean') { + constraint = required ? constraint.required() : constraint.optional(); + } + if (typeof description === 'string') { + constraint = constraint.description(description); + } + this.constraints.queryParams[paramName] = constraint; + cfg = constraint.describe(); + required = Boolean(cfg.flags && cfg.flags.presense === 'required'); + description = cfg.description; + type = cfg.type; + if ( + type === 'number' && + _.isArray(cfg.rules) && + _.some(cfg.rules, function (rule) { + return rule.name === 'integer'; + }) + ) { + type = 'integer'; + } + } + this.docs.addQueryParam( paramName, - attributes.description, - attributes.type, - attributes.required, + description, + type, + required, attributes.allowMultiple ); return this;