nverwer / roaster

Open API Router for eXist

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

roaster router logo

roaster

Test and Release semantic-release

OpenAPI Router for eXist

Reads an OpenAPI 3.0 specification from JSON and routes requests to handler functions written in XQuery.

It is a generic router to be used in any exist-db application. Since it is also the routing library used by TEI Publisher 7 you will find some examples referring to it.

TEI Publisher API

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.

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

Demo App

The demo app 'Roasted' is a barebones eXist-db application using the Roaster router. It serves a good starting-point for playing, learning and as a 'template' for your own apps.

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

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 port, edit .existdb.json.

Run the test suite with

npm install
npm test

More extensive tests for this package are contained in the tei-publisher-app repository.

How it works

eXist applications usually have a controller as main entry point. The controller.xql in TEI Publisher 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.

TEI Publisher uses two specifications: api.json and custom-api.json. This 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. The XQuery function must be resolved by the $lookup function in one of the modules which are visible at the point where roaster:route is called. Consequently, api.xql imports all modules containing handler functions.

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: PUT, POST, GET, ...
  • 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" }
    ))
};

Limitations

The library does not support $ref references in the Open API specification.

About

Open API Router for eXist

License:GNU General Public License v3.0


Languages

Language:XQuery 62.0%Language:JavaScript 36.6%Language:HTML 1.3%Language:Shell 0.1%