Skip to content

eeditiones/roaster

Repository files navigation

Roaster

roaster router logo

Define your API, then implement it.

Test and Release semantic-release

OpenAPI Router for eXist

Roaster is a generic router to be used in any exist-db application. It reads an OpenAPI 3.0 specification from a JSON file and routes requests to handler functions written in XQuery.

Roasted API

From any valid API specification you can generate an interactive documentation as the one above. This is also very helpful for exploratory testing of your implementation (see demo app).

Roaster is the routing library powering TEI Publisher. So, make sure to also check there for additional documentation and examples.

Installation

This library XAR can be either downloaded from the releases and is also available on eXist-db's public package repository.

How it works

eXist applications usually have a controller as main entry point. The controller.xql in the example application only handles requests to static resources, but forwards all other requests to an XQuery script api.xql. This script imports the OpenAPI router module and calls roaster:route, passing it one or more Open API specifications in JSON format.

The demo app, included in this repository, uses two specifications:

  • api.json demonstrates basic usage like parameters in path, query and body as well as file up- and downloads
  • api-jwt.json introduces more advanced use-cases like custom authentication and middlewares

Splitting up your api specifications can help keeping each focussed on specific tasks and also split up really long ones.

TEI Publisher has api.json and custom-api.json. There, it is done to make it easier for users to extend the default API. It is also possible to overwrite a route from api.json by placing it into custom-api.json.

Each route in the specification must have an operationId property. This is the name of the XQuery function that will handle the request to the given route. Several routes can use the same handler function, where applicable. The XQuery function will be resolved by the lookup-function passed to roaster:route. In order for that to work all route handler functions need to be available in the context of that function. This is why api.xql imports all modules containing handler functions.

Route Handling

The XQuery handler function must expect exactly one argument: $request as map(*). This is a map with a number of keys:

  • id: a uuid identifying this request (useful to find this exact request in your logfile)
  • parameters: a map containing all parameters (path and query) which were defined in the spec. The key is the name of the parameter, the value is the parameter value cast to the defined target type.
  • body: the body of the request (if requestBody was used), cast to the specified media type (currently application/json or application/xml).
  • config: the JSON object corresponding to the Open API path configuration for the current route and method
  • user: contains the authenticated user, if any authentication was successful
  • method: GET, POST, PUT, DELETE, HEAD...
  • path: the requested path
  • spec: the entire API definition this route is defined in

For example, here's a simple function which just echoes the passed in parameters:

declare function custom:echo($request as map(*)) {
    $request?parameters
};

Responses

If the function returns a value, it is sent to the client with a HTTP status code of 200 (OK). The returned value is converted into the specified target media type (if any, otherwise application/xml is assumed).

To modify responses like HTTP status code, body and headers the handler function may call roaster:response as its last operation.

  • roaster:response($code as xs:int, $data as item()*)
  • roaster:response($code as xs:int, $mediaType as xs:string?, $data as item()*)
  • roaster:response($code as xs:int, $mediaType as xs:string?, $data as item()*, $headers as map(*)?)

Example:

declare function custom:response($request as map(*)) {
    roaster:response(427, "application/octet-stream", "101010", 
      map { "x-special": "23", "Content-Length" : "1" })
};

Error Handling

If an error is encountered when processing the request, a JSON record is returned.

Example:

{
  "module": "/db/apps/oas-test/modules/api.xql",
  "code": "errors:NOT_FOUND_404",
  "value": "error details",
  "line": 34,
  "column": 5,
  "description": "document not found"
}

Request handlers can also throw explicit errors using the variables defined in errors.xql

Example:

error($errors:NOT_FOUND, "HTML file " || $path || " not found", map { "info": "additional info"})

The server will respond with the HTTP status code 404 to the client. The description and additional information will be added to the data that is sent.

However, for some operations you may want to handle an error instead of just returning it to the client. In this case use the extension property x-error-handler inside an API path item. It should contain the name of an error handler function, which is expected to take exactly one argument, a map(*).

Authentication

basic and cookie authentication are supported by default when the two-parameter signature of roaster:router is used. The key based authentication type corresponds to eXist's persistent login mechanism and uses cookies for the key. To enable it, use the following securityScheme declaration:

"components": {
    "securitySchemes": {
        "cookieAuth": {
            "type": "apiKey",
            "name": "org.exist.login",
            "in": "cookie"
        }
    }
}

The security scheme must be named cookieAuth. The name property defines the login session name to be used.

Custom authentication strategies are possible. The test application has an example for JSON Web Tokens.

Access Constraints

Certain operations may be restricted to defined users or groups. We use an implementation-specific property, 'x-constraints' for this on the operation level, e.g.:

"/api/upload/{collection}": {
  "post": {
    "summary": "Upload a number of files",
    "x-constraints": {
        "groups": "dba"
    }
  }
}

requires that the effective user or real user running the operation belongs to the "tei" group. The effective user will be used, if present.

groups can be an array, too. In that case the user must be in at least one of them.

{ "groups": ["tei", "dba"] }

This will work also for custom authorization strategies. The handler function needs to extend the request map with the user information.

Middleware

If you need to perform certain actions on each request you can add a transformation function also known as middleware.

Most internal operations that construct the $request map passed to your operations are such functions. Authorization is a middleware as well.

A middleware has two parameters of type map, the current request map and the current response, and returns two map that will become the request and response maps for the next transformation.

Example middleware that adds a "beep" property to each request and a custom x-beep header to each response:

declare function custom-router:use-beep-boop ($request as map(*), $response as map(*)) as map(*) {
    (: extend request :)
    map:put($request, "beep", "boop"),
    (: add custom header to all responses :)
    map:put($response, $router:RESPONSE_HEADERS, map:merge((
      $response?($router:RESPONSE_HEADERS),
      map { "x-beep": "boop" }
    ))
};

File Uploads

Roaster transparently handles data from multipart/form-data requests to keep route handlers short and readable. Please see the file upload documentation for more details on this.

Limitations

The library does not support yet support following OpenAPI feature(s):

  • $ref references in the Open API specification (issue)

Development

Clone this repository and switch to your local working directory.

Requirements

Building and Installation

Roaster uses Gulp as its build tool which itself builds on NPM. To initialize the project and load dependencies run

npm i

Note: the install commands below assume that you have a local eXist-db running on port 8080. However the database connection can be modified in .existdb.json.

Run Description
gulp build to just build the roaster routing lib.
gulp build:all to build the routing lib and the demo app.
gulp install To build and install the lib in one go
gulp install:all To build and install lib and demo app run

The resulting xar(s) are found in the root of the project.

An ant-task is still defined, but will use gulp in the end (through npm run build).

Demo App

The repository contains a demo and test application, 'Roasted', which is using the Roaster router. It serves a good starting-point for playing, learning and as a 'template' for your own apps.

The demo app is now available for download as an artefact of a release.

  1. download the roasted.xar

  2. install it in your eXist-db instance

    You can use the upload feature of the dashboard. The roaster library will be installed as a dependency in the required version.

  3. open http://localhost:8080/exist/apps/roasted/

    If your instance is running on a different domain replace localhost:8080 with the correct one. This will open a form dynamically created from the definition files api.json and api-jwt.json.

Building the demo application from source

If you want to make modifications to the demo app and test them

  1. gulp install:all

    will create both the library and the testapp XAR and install them.

  2. open http://localhost:8080/exist/apps/roasted/

    This will open a form dynamically created from the definition files api.json and api-jwt.json.

Development

Running gulp watch will build and install the library and watch for file changes. Whenever one of the watched files is changed a fresh version of the xar will be installed in the database. This included the test application in test/app.

Testing

To run the local test suite you need an instance of eXist running on localhost:8080 and npm to be available in your path. To test against a different different server, or use a different user or password you can copy .env.example to .env and edit it to your needs.

Run the test suite with

npm test

Additional tests that cover this package are contained in the tei-publisher-app repository.

Contributing

Roaster uses Angular Commit Message Conventions to determine semantic versioning of releases, see these examples:

Commit message Release type
fix(pencil): stop graphite breaking when too much pressure applied Patch Release
feat(pencil): add 'graphiteWidth' option Minor Feature Release
perf(pencil): remove graphiteWidth option

BREAKING CHANGE: The graphiteWidth option has been removed.
The default graphite width of 10mm is always used for performance reasons.
Major Breaking Release