Skip to content
Erik Huelsmann edited this page Jul 19, 2022 · 22 revisions

API endpoint description

The endpoints are described in OpenAPI 3.0 stored in YAML files. The choice for OpenAPI 3.0 is determined by the fact that we use JSONSchema::Validator which currently does not provide OAS 3.1 support (yet). The library which does support OAS3.1 support (JSON::Schema::Modern), imports Mojolicious, which is a full web framework that we currently don't depend on.

Topics to be worked on

  • Factor commonality into shared description
  • Definition of response codes
  • Factor out boiler plate from all APIs
  • Generate documentation using OpenAPI 3.0 input files
  • Definition of the use of filter criteria
  • Definition of the interaction of resources with workflows
  • [ ]

Relationship between the UI and the API

In REST, there's the concept of HATEOAS: the idea that the client knows how to render the content sent by the server without encoding business logic into the client. For the scope of LedgerSMB, "full HATEOAS" as described in ... is over-engineering for LedgerSMB. The goal that HATEOAS intend to achieve is a goal for the design of our API as well: separating the UI from the underlying business process, using the API not only to convey state of that process, but also to convey available next steps in it.

The API implements the list of available next actions - similar to how it's done with many APIs implementing HATEOAS - by sending a _links section in our resources, e.g. for a warehouse:

{
   "_links": [
     { "rel": "self", "href": "/warehouses/1" },
     { "rel": "delete", "href": "/warehouses/1", "method": "DELETE" },
     { "rel": "modify", "href": "/warehouses/1", "method": "PATCH" },
   ],
   "description": "Warehouse 1",
   "id": 1,
}

The above resource indicates to the UI that the available next actions on the resource are "delete" and "modify".

The idea here is to keep the number of possible rel types low in order to keep complexity on the client low. Much more elaborate examples for invoices are included below which show the concept of using a single rel type for a series of actions that can be performed based on the current resource.

Input

Media types

Web service requests currently support exactly one input media type: application/json. End points intended for file uploads accept a wide range of media types as an exception.

Validation

Incoming requests have the following validations applied:

  • Maximum request body
  • JSON errors
  • Schema correctness (e.g. required fields)
  • Functional correctness (e.g. existing (cross) references to data entities)

Each of these phases can generate one or more errors. Per phase, the server will determine as many errors as possible and report back all errors found.

Output

Media types

Validation

Idempotency (or duplicate request prevention)

Requests on existing resources will be managed for "idempotency" using ETag headers: Every response on an existing or created resource returns an ETag header which must be used by modifying (PUT/PATCH/POST) service calls to uniquely identify the resource version being modified. Creation requests are a different matter: since there is no resource yet, there's no ETag to be retrieved from the server in order to be able to send a modification request. The solution here is to add a "creation UUID" to each creation request. The server checks the creation UUID on each creation request against the UUIDs in the system. When there's a match, the submitted data is compared to the state of the existing resource. When the resource state matches (the definition of a match may be resource specific), the existing resource is returned. When the state does not match, an error 422 (Unprocessable request) is returned.

Invoice examples for links sections

A SAVED invoice

{
   "_links": [
      { "rel": "self", "href": "/invoices/3" },
      { "rel": "delete", "href": "/invoices/3", "method": "DELETE" },
      { "rel": "modify", "href": "/invoices/3", "method": "PUT" },
      { "rel": "transition", "href": "/invoices/3/transitions", "method": "POST", "transition": "post" },
      { "rel": "transition", "href": "/invoices/3/transitions", "method": "POST", "transition": "to-sales-order" },
      { "rel": "transition", "href": "/invoices/3/transitions", "method": "POST", "transition": "to-purchase-order" },
   ],
   {
      "id": 3,
      "state": "SAVED",
      "..."
   }
}

A POSTED invoice

{
   "_links": [
      { "rel": "self", "href": "/invoices/3" },
      { "rel": "delete", "href": "/invoices/3", "method": "DELETE" },
      { "rel": "modify", "href": "/invoices/3", "method": "PUT" },
      { "rel": "transition", "href": "/invoices/3/transitions", "method": "POST", "transition": "void" },
      { "rel": "transition", "href": "/invoices/3/transitions", "method": "POST", "transition": "e-mail" },
      { "rel": "transition", "href": "/invoices/3/transitions", "method": "POST", "transition": "copy" },
      { "rel": "transition", "href": "/invoices/3/transitions", "method": "POST", "transition": "to-sales-order" },
      { "rel": "transition", "href": "/invoices/3/transitions", "method": "POST", "transition": "to-purchase-order" },
   ],
   {
      "id": 3,
      "state": "POSTED",
      "..."
   }
}