Skip to content

Latest commit

 

History

History
815 lines (632 loc) · 25 KB

File metadata and controls

815 lines (632 loc) · 25 KB
code type order title meta
false
page
300
API Controllers | Develop on Kuzzle | Guide | Core
name content
description
Extend Kuzzle API with controllers and actions
name content
keywords
Kuzzle, Documentation, kuzzle write pluggins, General purpose backend, iot, backend, opensource, API Controllers

API Controllers

Kuzzle allows to extend its existing API using Controllers. Controllers are logical containers of actions.

These actions are then processed like any other API action and can be executed through the different mechanisms to secure and normalize requests.

Add a new Controller

Each controller can therefore have several actions. Each of these actions is associated with a function called handler.

::: info The syntax of the definition of these actions and the associated handlers is defined by the ControllerDefinition interface. :::

By convention, a controller action is identified with the name of the controller followed by the action separated by a colon: <controller>:<action> (e.g. document:create).

::: warning Controllers must be added to the application before the application is started with the Backend.start method. :::

We have chosen to allow developers to add controllers in two different ways in order to best adapt to their needs.

These two ways are very similar and achieve the same goal.

Register a Controller

The Backend.controller.register method allows to add a new controller using a Plain Old Javascript Object (POJO) complying with the ControllerDefinition interface.

This method takes in parameter the name of the controller followed by its definition:

app.controller.register('greeting', {
  actions: {
    sayHello: {
      handler: async request => /* ... */
    },
    sayGoodbye: {
      handler: async request => /* ... */
    },
  }
});

This is faster to develop but maintenance can be costly in the long run for larger applications with many controllers and actions.

Use a Controller class

The Backend.controller.use method allows to add a new controller using a class inheriting from the Controller class.

This class must respect the following conventions:

  • extend the Controller class
  • call the super constructor with the application instance
  • define the controller actions under the definition property

::: info The controller name will be inferred from the class name (unless the name property is defined). E.g. PaymentSolutionController will become payment-solution. :::

import { Controller, KuzzleRequest } from 'kuzzle';

class GreetingController extends Controller {
  constructor (app: Backend) {
    super(app);

    // type ControllerDefinition
    this.definition = {
      actions: {
        sayHello: {
          handler: this.sayHello
        },
        sayGoodbye: {
          handler: this.sayGoodbye
        }
      }
    };
  }

  async sayHello (request: KuzzleRequest) { /* ... */ }

  async sayGoodbye (request: KuzzleRequest) { /* ... */ }
}

::: info If the handler function is an instance method of the controller then the context will be automatically bound to the controller instance. :::

Once you have defined your controller class, you can instantiate it and pass it to the Backend.controller.use method:

const greetingController = new GreetingController(app);

app.controller.use(greetingController);

This way of doing things takes longer to develop but it allows you to have a better code architecture while respecting OOP concepts.

Handler Function

The handler is the function that will be called each time our API action is executed.

This function takes a KuzzleRequest object as a parameter and must return a Promise resolving on the result to be returned to the client.

This function is defined in the handler property of an action. Its signature is: (request: KuzzleRequest) => Promise<any>.

app.controller.register('greeting', {
  actions: {
    sayHello: {
      // Handler function for the "greeting:sayHello" action
      handler: async (request: KuzzleRequest) => {
        return `Hello, ${request.getString('name')}`;
      }
    }
  }
});

The result returned by our handler will be converted to JSON format and integrated into the standard Kuzzle response in the result property.

npx wscat -c ws://localhost:7512 --execute '{
  "controller": "greeting",
  "action": "sayHello",
  "name": "Yagmur"
}'

{
  "requestId": "a6f4f5b6-1aa2-4cf9-9724-12b12575c047",
  "status": 200,
  "error": null,
  "controller": "greeting",
  "action": "sayHello",
  "collection": null,
  "index": null,
  "volatile": null,
  "result": "Hello, Yagmur", # <= handler function return value
  "room": "a6f4f5b6-1aa2-4cf9-9724-12b12575c047"
}

HTTP routes

The execution of an API action through the HTTP protocol is significantly different from other protocols.

Indeed, the HTTP protocol uses verbs and routes in order to address an action whereas the other protocols only use the controller and action name in their JSON payloads.

Define a HTTP route

When defining a controller action, it is also possible to specify one or more HTTP routes available to execute our action using the http property.

This property is at the same level as handler and represents an array of routes. Each route is an object containing a verb and a path property.

The following HTTP verbs are available: get, post, put, delete, head.

::: info When the path property starts with a / then the route is added as is, otherwise the route will be prefixed with /_/. :::

app.controller.register('greeting', {
  actions: {
    sayHello: {
      handler: async (request: KuzzleRequest) => {
        return `Hello, ${request.getString('name')}`;
      },
      http: [
        // generated route: "GET http://<host>:<port>/greeting/hello"
        { verb: 'get', path: '/greeting/hello' },
        // generated route: "POST http://<host>:<port>/_/hello/world"
        { verb: 'post', path: 'hello/world' },
      ]
    }
  }
});

::: warning It is recommended to let Kuzzle prefix the routes with /_/ in order to avoid conflict with the existing routes of the standard API. :::

It is possible to define paths with url parameters. These parameters will be captured and then integrated into the KuzzleRequest Input.

app.controller.register('greeting', {
  actions: {
    sayHello: {
      handler: async (request: KuzzleRequest) => {
        // "name" comes from the url parameter
        return `Hello, ${request.getString('name')}`;
      },
      http: [
        { verb: 'get', path: '/email/send/:name' },
      ]
    }
  }
});

Default route

If the http property is not set, then Kuzzle will generate a default route so that the action can be called from the HTTP protocol.

This default generated route has the following format: GET http://<host>:<port>/_/<controller-name>/<action-name>.

The name of the controller and the action will be converted to kebab-case format. For example the default route of the sayHello action will be: GET http://<host>:<port>/_/greeting/say-hello.

::: info It is possible to prevent the generation of a default HTTP route by providing an empty array to the http property. By doing this, the action will only be available through the HTTP protocol with the JSON Query Endpoint. :::

OpenAPI Specification

The API action server:openapi returns available API routes OpenAPI v3 specifications.

When used with the scope argument to app, the API action will returns the OpenAPI specification of custom controllers added by plugins or the application.

Kuzzle generates specifications for custom API actions, although it is possible to customize the OpenAPI specifications for each HTTP route.

To register this custom specification, it must be declared with http routes in an openapi property. To write this object, follow the official openapi specification especially the paths object section.

app.controller.register('greeting', {
  actions: {
    sayHello: {
      handler: async (request: KuzzleRequest) => {
        return `Hello, ${request.getString('name')}`;
      },
      http: [{
        verb: 'post',
        path: 'hello/world',
        openapi: {
          description: "Simply say hello",
          parameters: [{
            in: "query",
              name: "name",
              schema: {
                type: "string"
              },
              required: true,
          }],
          responses: {
            200: {
              description: "Custom greeting",
              content: {
                "application/json": {
                  schema: {
                    type: "string",
                  }
                }
              }
            }
          }
        }
      }]
    }
  }
});

Then Kuzzle will inject the http route specification as shown in the example below using each property path, verb and openapi.

{
  "openapi": "3.0.1",
  "info": {
    "title":"Kuzzle API",
    "description":"The Kuzzle HTTP API",
    "contact": {
      "name":"Kuzzle team",
      "url":"http://kuzzle.io",
      "email":"hello@kuzzle.io"
    },
    "license": {
      "name":"Apache 2",
      "url":"http://opensource.org/licenses/apache2.0"
    },
    "version":"2.4.5"
  },
  "paths": {
    "<path>": {
      "<verb>": {
        // openapi property injected here
      }
    },
  }
}

The complete OpenAPI definition is accessible and customizable with the Backend.openapi.definition property.

Example: Register an OpenAPI schema

app.openApi.definition.components.LogisticObjects = {
  Item: {
    type: 'object',
    properties: {
      name: { type: 'string' },
      quantity: { type: 'integer' },
    }
  }
};

// Then you can reference this schema anywhere according to OpenAPI specification "#/components/LogisticObjects/Item"

KuzzleRequest Input

The handler of an API action receives an instance of KuzzleRequest object. This object represents an API request and contains both the client input and client contextual information.

The arguments of requests sent to the Kuzzle API are available in the KuzzleRequest.input property.

The main available properties are the following:

  • controller: API controller name
  • action: API action name
  • args: Action arguments
  • body: Body content

Extract parameters from request

The request object exposes methods to safely extract parameters from the request in a standardized way.

Each of those methods will check for the parameter presence and type. In case of a validation failure, the corresponding API error will be thrown.

All those methods start with getXX: getString, getBoolean, getBodyObject etc.

HTTP

With HTTP, there are 3 types of input parameters:

  • URL parameters (e.g. /greeting/hello/:name)
  • Query arguments (e.g. /greeting/hello?name=aschen)
  • KuzzleRequest body

URL parameters and query arguments can be found in the request.input.args property.

The content of the query body can be found in the request.input.body property

::: info The request body must either be in JSON format or submitted as an HTTP form (URL encoded or multipart form data) :::

For example, with the following request input:

# Route: "POST /greeting/hello/:name"
curl \
  -X POST \
  -H  "Content-Type: application/json" \
  "localhost:7512/_/greeting/hello/aschen?_id=JkkZN62jLSA&age=27" \
  --data '{
    "city" : "Antalya"
  }'

We can retrieve them in the KuzzleRequest object passed to the handler:

import assert from 'assert';

app.controller.register('greeting', {
  actions: {
    sayHello: {
      handler: async (request: KuzzleRequest) => {
        assert(request.input.args._id === 'JkkZN62jLSA');
        assert(request.input.args.name === 'aschen');
        assert(request.input.args.age === '27');
        assert(request.input.body.city === 'Antalya');
        // equivalent to
        assert(request.getId() === 'JkkZN62jLSA');
        assert(request.getString('name') === 'aschen');
        assert(request.getInteger('age') === '27');
        assert(request.getBodyString('city') === 'Antalya');
      },
      http: [
        { verb: 'POST', path: 'greeting/hello/:name' }
      ]
    }
  }
});

::: info See the KuzzleRequest Payload page for more information about using the API with HTTP. :::

Other protocols

Other protocols directly use JSON payloads.

These payloads contain all the information directly:

npx wscat -c ws://localhost:7512 --execute '{
  "controller": "greeting",
  "action": "sayHello",
  "_id": "JkkZN62jLSA",
  "age": 27,
  "name": "aschen",
  "body": {
    "city": "Antalya"
  }
}'

We can retrieve them in the KuzzleRequest object passed to the handler:

import assert from 'assert';

app.controller.register('greeting', {
  actions: {
    sayHello: {
      handler: async (request: KuzzleRequest) => {
        assert(request.input.args._id === 'JkkZN62jLSA');
        assert(request.input.args.name === 'aschen');
        assert(request.input.args.age === '27');
        assert(request.input.body.city === 'Antalya');
        // equivalent to
        assert(request.getId() === 'JkkZN62jLSA');
        assert(request.getString('name') === 'aschen');
        assert(request.getInteger('age') === '27');
        assert(request.getBodyString('city') === 'Antalya');
      },
    }
  }
});

::: info See the KuzzleRequest Payload page for more information about using the API with other protocols. :::

KuzzleRequest Context

Information about the client that executes an API action are available in the KuzzleRequest.context property.

The available properties are as follows:

  • connection: information about the connection
  • user: information about the user executing the request
  • (optional) token: information about the authentication token

Example:

import assert from 'assert';

app.controller.register('greeting', {
  actions: {
    sayHello: {
      handler: async (request: KuzzleRequest) => {
        assert(request.context.connection.protocol === 'http');
        // Unauthenticated users are anonymous
        // and the anonymous user ID is "-1"
        assert(request.context.user._id === '-1');
        // equivalent to
        assert(request.getKuid() === '-1');
      },
    }
  }
});

::: info More informations about the RequestContext class properties. :::

Response format

Kuzzle Response are standardized. This format is shared by all API actions, including custom controller actions.

A ResponsePayload is a JSON object with the following format:

Property Description
action API action
collection Collection name, or null if no collection was involved
controller API controller
error KuzzleError object, or null if there was no error
index Index name, or null if no index was involved
requestId KuzzleRequest unique identifier
result Action result, or null if an error occured
status Response status, using HTTP status codes
volatile Arbitrary data repeated from the initial request
The result property will contain the return of the action handler function.

For example, when calling this controller action:

app.controller.register('greeting', {
  actions: {
    sayHello: {
      handler: async (request: KuzzleRequest) => {
        return `Hello, ${request.getString('name')}`;
      }
    }
  }
});

The following response will be sent:

npx wscat -c ws://localhost:7512 --execute '{
  "controller": "greeting",
  "action": "sayHello",
  "name": "Yagmur"
}'

{
  "requestId": "a6f4f5b6-1aa2-4cf9-9724-12b12575c047",
  "status": 200,
  "error": null,
  "controller": "greeting",
  "action": "sayHello",
  "collection": null,
  "index": null,
  "volatile": null,
  "result": "Hello, Yagmur", # <= handler function return value
  "room": "a6f4f5b6-1aa2-4cf9-9724-12b12575c047"
}

Return a custom response

In some cases it may be necessary to return a response that differs from the standard API response format.

This may be to send a smaller JSON response for constrained environments, to perform HTTP redirection or to return another MIME type such as CSV, an image, a PDF document, etc.

For this it is possible to use the method request.response.configure with the raw format. This option prevents Kuzzle from standardizing an action's output:

Example: Return a CSV file

app.controller.register('files', {
  actions: {
    csv: {
      handler: async request => {
        const csv = 'name,age\naschen,27\ncaner,28\n';

        request.response.configure({
          format: 'raw',
          headers: {
            'Content-Type': 'text/csv',
            'Content-Disposition': 'attachment; filename="export.csv"'
          }
        });

        return csv;
      }
    }
  }
});

The response will only contain the CSV document:

curl localhost:7512/_/files/csv

name,age
aschen,27
caner,28

You can also change the HTTP status code with the status option.

Example: Redirect requests to another website

app.controller.register('redirect', {
  actions: {
    proxy: {
      handler: async request => {
        request.response.configure({
          format: 'raw',
          // HTTP status code for redirection
          status: 302,
          headers: {
            'Location': 'http://kuzzle.io'
          }
        });

        return null;
      }
    }
  }
});

HTTP Streams

Kuzzle sends response through HTTP using the JSON format. Kuzzle Response are standardized. This format is shared by all API actions, including custom controller actions.

Kuzzle Response might be heavy when it comes to processing and sending large volumes of data, since the response are sent in one go, this imply that all the processing must be done before sending the response and must be stored in ram until the whole response is sent.

To avoid having to process and store large amount of data before sending it, Kuzzle allow controller's actions to return an HttpStream instead of a JSON object. Kuzzle will then stream the data though the HTTP protocol in chunk until the stream is closed, this way you can process bits of your data at a time and not have everything stored in ram.

::: warning Chunks are sent through the HTTP Protocol each time a chunk is emitted through the data event of the given stream. It's up to you to implement a buffer mechanism to avoid sending too many small consecutive chunks through the network.

Sending too many small chunks instead of bigger chunks will increase the number of syscall made to the TCP Socket and might decrease performance and throughput. :::

Usage:

All you need to send a stream from any controller's actions is to wrap any Readable Stream from NodeJS with an HttpStream.

Example: Read a file from the disk and send it.

const fs = require('fs');

app.controller.register('myController', {
  actions: {
    myDownloadAction: {
      handler: async (request: KuzzleRequest) => {
        const readStream = fs.createReadStream('./Document.tar.gz');

        return new HttpStream(readStream);
      }
    }
  }
});

Use a custom Controller Action

As we have seen, controller actions can be executed via different protocols.

We will explore the various possibilities available to execute API actions.

app.controller.register('greeting', {
  actions: {
    sayHello: {
      handler: async (request: KuzzleRequest) => {
        return `Hello, ${request.getString('name')}`;
      }
    }
  }
});

HTTP

Our action can be executed through the HTTP protocol by using an HTTP client (cURL, HTTPie, Postman, ...):

curl http://localhost:7512/_/greeting/say-hello?name=Yagmur

::: info Default generated routes use the GET verb. It is therefore possible to open them directly in a browser: http://localhost:7512/_/greeting/say-hello?name=Yagmur :::

WebSocket

To execute our action through the WebSocket protocol, we will be using wscat:

npx wscat -c ws://localhost:7512 --execute '{
  "controller": "greeting",
  "action": "sayHello",
  "name": "Yagmur"
}'

Kourou

From a terminal, Kourou, the Kuzzle CLI, can be used to execute an action:

kourou greeting:sayHello --arg name=Yagmur

It is possible to pass multiple arguments by repeating the --arg <arg>=<value> flag or specify a body with the --body '{}' flag.

::: info More info about Kourou. :::

SDK

From one of our SDKs, it is possible to use the query method which takes a KuzzleRequest Payload as a parameter.

:::: tabs ::: tab Javascript

Using the Javascript SDK Kuzzle.query method:

const response = await kuzzle.query({
  controller: 'greeting',
  action: 'sayHello',
  name: 'Yagmur'
});

::: ::: tab Dart

Using the Dart SDK Kuzzle.query method:

final response = await kuzzle.query({
  'controller': 'greeting',
  'action': 'sayHello',
  'name': 'Yagmur'
});

:::

::: tab Kotlin

Using the JVM SDK Kuzzle.query method:

ConcurrentHashMap<String, Object> query = new ConcurrentHashMap<>();
query.put("controller", "greeting");
query.put("action", "sayHello");
query.put("name", "Yagmur");

Response res = kuzzle.query(query).get();

:::

::: tab Csharp

Using the Csharp SDK Kuzzle.query method:

JObject request = JObject.Parse(@"{
  controller: 'greeting',
  action: 'sayHello',
  name: 'Yagmur'
}");

Response response = await kuzzle.QueryAsync(request);

:::

::::

Allow access to a custom Controller Action

In the rights management system, roles are managing access to API actions.

They operate on a whitelist principle by listing the controllers and actions they give access to.

So, to allow access to the greeting:sayHello action, the following role can be written:

kourou security:createRole '{
  controllers: {
    greeting: {
      actions: {
        sayHello: true
      }
    }
  }
}' --id steward

It is also possible to use a wildcard (*) to give access to all of a controller's actions:

kourou security:createRole '{
  controllers: {
    greeting: {
      actions: {
        "*": true
      }
    }
  }
}' --id steward

::: info More info about Permissions :::