Support route creation with path parameters and secure keys. (#114)
authorJames Thomas <jthomas.uk@gmail.com>
Sat, 28 Apr 2018 18:42:31 +0000 (19:42 +0100)
committerCarlos Santana <csantanapr@apache.org>
Sat, 28 Apr 2018 18:42:31 +0000 (14:42 -0400)
* 3.11.0

* 3.12.0

* Fixing #100

Connect echo action to trigger with rule to ensure result has
activation identifier.

* 3.13.1

* Support route creation with path parameters and secure keys.

Parse path parameters from route options and generate correct swagger.
New creation parameter to pass authentication key for secured web actions.

README.md
lib/routes.js
test/unit/routes.test.js

index 280440f..65671b3 100644 (file)
--- a/README.md
+++ b/README.md
@@ -490,6 +490,7 @@ The following optional parameters are supported:
 - `responsetype` - content type returned by web action, possible values: `html`, `http`, `json`, `text` and `svg` (default: `json`).
 - `basepath` - base URI path for endpoints (default: `/`)
 - `name` - identifier for API (default: `basepath`)
+- `secure_key` - auth key for secure web action
 
 ### add route (swagger)
 
index a40e85e..6c1eaf1 100644 (file)
@@ -104,6 +104,12 @@ class Routes extends BaseOperation {
       action: this.routeSwaggerAction(params)
     }
 
+    const pathParameters = this.parsePathParameters(params.relpath)
+
+    if (pathParameters.length) {
+      apidoc.pathParameters = pathParameters.map(this.createPathParameter)
+    }
+
     if (params.name) {
       apidoc.apiName = params.name
     }
@@ -117,13 +123,20 @@ class Routes extends BaseOperation {
     if (params.action.startsWith('/')) {
       namespace = names.parseNamespace(params.action)
     }
-    return {
+
+    const body = {
       name: id,
       namespace: namespace,
       backendMethod: `GET`,
       backendUrl: this.actionUrlPath(id, namespace),
       authkey: this.client.options.apiKey
     }
+
+    if (params.secure_key) {
+      body.secureKey = params.secure_key
+    }
+
+    return body
   }
 
   routeBasepath (params) {
@@ -143,6 +156,36 @@ class Routes extends BaseOperation {
   hasAccessToken () {
     return !!this.client.options.apigwToken
   }
+
+  // return list of path parameters from paths
+  // e.g. /book/{id}
+  // Multiple parameters are supported.
+  parsePathParameters (path) {
+    const regex = /{([^}]+)\}/g
+    const findAllParams = p => {
+      const ids = []
+      let id = regex.exec(p)
+      while (id) {
+        ids.push(id[1])
+        id = regex.exec(p)
+      }
+      return ids
+    }
+
+    return path.split('/')
+      .map(findAllParams)
+      .reduce((sum, el) => sum.concat(el), [])
+  }
+
+  createPathParameter (name) {
+    return {
+      name: name,
+      in: 'path',
+      description: `Default description for '${name}'`,
+      required: true,
+      type: 'string'
+    }
+  }
 }
 
 module.exports = Routes
index 30eab78..e813a99 100644 (file)
@@ -391,6 +391,42 @@ test('should create a route with response type', t => {
   return routes.create({relpath: '/hello', operation: 'GET', action: 'helloAction', responsetype: 'http'})
 })
 
+test('should create a route with secure key', t => {
+  t.plan(3)
+  const pathUrl = path => `https://openwhisk.ng.bluemix.net/api/v1/${path}`
+  const apiKey = 'username:password'
+  const secureKey = 'some_key'
+  const clientOptions = {apiKey}
+  const client = {pathUrl, options: clientOptions}
+
+  const body = {
+    apidoc: {
+      namespace: '_',
+      gatewayBasePath: '/',
+      gatewayPath: '/hello',
+      gatewayMethod: 'GET',
+      id: 'API:_:/',
+      action: {
+        name: 'helloAction',
+        namespace: '_',
+        backendMethod: 'GET',
+        backendUrl: 'https://openwhisk.ng.bluemix.net/api/v1/web/_/default/helloAction.http',
+        authkey: apiKey,
+        secureKey: secureKey
+      }
+    }
+  }
+
+  client.request = (method, path, _options) => {
+    t.is(method, 'POST')
+    t.is(path, routes.routeMgmtApiPath('createApi'))
+    t.deepEqual(_options.body, body)
+  }
+
+  const routes = new Routes(client)
+  return routes.create({relpath: '/hello', operation: 'GET', action: 'helloAction', secure_key: secureKey})
+})
+
 test('should create a route with apigwToken and action with package', t => {
   t.plan(4)
   const pathUrl = path => `https://openwhisk.ng.bluemix.net/api/v1/${path}`
@@ -631,6 +667,72 @@ test('should create a route using action name with ns overriding defaults', t =>
   return routes.create({relpath: '/hello', operation: 'GET', action: '/test/helloAction'})
 })
 
+test('should create a route with path parameters', t => {
+  t.plan(3)
+  const pathUrl = path => `https://openwhisk.ng.bluemix.net/api/v1/${path}`
+  const apiKey = 'username:password'
+  const clientOptions = {apiKey}
+  const client = {pathUrl, options: clientOptions}
+
+  const body = {
+    apidoc: {
+      namespace: '_',
+      gatewayBasePath: '/',
+      gatewayPath: '/foo/{bar}/{baz}',
+      gatewayMethod: 'GET',
+      id: 'API:_:/',
+      action: {
+        name: 'helloAction',
+        namespace: '_',
+        backendMethod: 'GET',
+        backendUrl: 'https://openwhisk.ng.bluemix.net/api/v1/web/_/default/helloAction.http',
+        authkey: apiKey
+      },
+      pathParameters: [
+        {
+          name: 'bar',
+          in: 'path',
+          description: "Default description for 'bar'",
+          required: true,
+          type: 'string'
+        },
+        {
+          name: 'baz',
+          in: 'path',
+          description: "Default description for 'baz'",
+          required: true,
+          type: 'string'
+        }
+      ]
+    }
+  }
+
+  client.request = (method, path, _options) => {
+    t.is(method, 'POST')
+    t.is(path, routes.routeMgmtApiPath('createApi'))
+    t.deepEqual(_options.body, body)
+  }
+
+  const routes = new Routes(client)
+  return routes.create({relpath: '/foo/{bar}/{baz}', operation: 'GET', action: 'helloAction'})
+})
+
+test('should parse path parameters', t => {
+  const routes = new Routes()
+  t.deepEqual(routes.parsePathParameters('/foo/{bar}'), ['bar'])
+  t.deepEqual(routes.parsePathParameters('/{foo}/bar'), ['foo'])
+  t.deepEqual(routes.parsePathParameters('/{foo}/{bar}'), ['foo', 'bar'])
+
+  t.deepEqual(routes.parsePathParameters('/{foo}bar}'), ['foo'])
+  t.deepEqual(routes.parsePathParameters('/{foo}{bar}'), ['foo', 'bar'])
+  t.deepEqual(routes.parsePathParameters('/a{foo}c{bar}b'), ['foo', 'bar'])
+
+  t.deepEqual(routes.parsePathParameters('/foo/bar'), [])
+  t.deepEqual(routes.parsePathParameters('/{foo/bar}'), [])
+  t.deepEqual(routes.parsePathParameters('/foo/bar}'), [])
+  t.deepEqual(routes.parsePathParameters('/{foo/bar'), [])
+})
+
 test('create routes missing mandatory parameters', t => {
   const routes = new Routes()
   t.throws(() => { routes.create() }, /Missing mandatory parameters: relpath, operation, action/)