Skip to content
Erik Huelsmann edited this page Jul 29, 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 navigate 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 that process.

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 intent is for the client to know how to use these links from the links section and follow them appropriately. That is: a UI client knows how to present these actions to the user.

By providing these _links, the client doesn't need to know about authorizations extended to the user: the server knows about those and only sends those links which are available to the user in the context of the current request. The client only renders the links it has been handed. Since the client only knows about types of links and not about the links itself, there is great flexibility to the server to send links that the client doesn't have prior knowledge about: as long as the rel types are known to the client, it can render the actions as available actions to the user.

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.

Note: Actions available from the "_links" section may be rendered by a client, meaning that clients need not present all actions provided by the server; i.e. some actions may not be appropriate to the client's purpose. Another case where a client may decide not to render an available action from the "_links" section is if the "rel" type is unknown to it.

Link "rel" types

As mentioned above, building clients will be greatly simplified if and when the number of "rel" types (the values the "rel" field can take) is kept low: that way generic functionality can be used by clients to render the available actions.

Note: rel types may occur more than once in the "_links" section, unless explicitly documented otherwise. When there are multiple occurrences of the same type, the server must include a "title" attribute to hint the client about the distinction between each. It's encouraged to provide a "title" attribute on rel types, though.

Rel types on each resource

Each part of a resource that is also accessible as a stand-alone resource (which means at the very least each outer-most object on each resource should have it) must present the following "rel" type(s) in its "_links" section:

  • "self": has an "href" field which presents the (relative) URL under which the resource can be directly accessed

Next to the required rel type(s) above, the following rel types are available when appropriate:

  • "download": has an "href" and a "method" field and may specify a "content-type" field; the "method" field contains an HTTP verb to be used with the URL from the "href" while the "content-type" field specifies the value to populate the "Accept" header of the request with.

Rel types on collection resources

By "collection resource" it is meant the resource which publishes a set of individual resources of a specific type. E.g. /warehouses is the collection resource which presents the set of all individual warehouse resources.

The following rel types will be included in the "_links" section of a collection resource, when appropriate (i.e. when the requesting user is authorized, when the function exists in the application, etc):

  • "add": presents the url and HTTP method to use when creating one additional resource of the type held in the collection; this rel type includes an "href" (as discussed before) and a "method" field.

Note: The "delete" rel type should not be issued on the collection, but on the individual resources within the collection, unless it is acceptable from application and/or business process point of view that the collection itself is being deleted.

Rel types on non-collection resources

The following rel types will be included in the "_links" section of a non-collection resource, when appropriate (i.e. when the requesting user is authorized, when the function exists in the application, etc):

  • "delete": presents the URL and HTTP method to use when removing the resource (similar to the "add" method on collection resources)
  • "modify": presents the URL and HTTP method to use when changing the content of the resource

Some resources represent entities which have an associated or embedded workflow. This type of resources may include the following rel types:

  • "action": has "href" and "method" fields to be used when triggering the action (= state transition) and an "action" field which contains the programmatic identification of the action

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": "action", "href": "/invoices/3/transitions", "method": "POST", "action": "post" },
      { "rel": "action", "href": "/invoices/3/transitions", "method": "POST", "action": "to-sales-order" },
      { "rel": "action", "href": "/invoices/3/transitions", "method": "POST", "action": "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": "action", "href": "/invoices/3/transitions", "method": "POST", "action": "void", "title": "Void" },
      { "rel": "action", "href": "/invoices/3/transitions", "method": "POST", "action": "e-mail", "title": "E-mail" },
      { "rel": "action", "href": "/invoices/3/transitions", "method": "POST", "action": "copy", "title": "Copy to New" },
      { "rel": "action", "href": "/invoices/3/transitions", "method": "POST", "action": "to-sales-order", "title": "Sales Order" },
      { "rel": "action", "href": "/invoices/3/transitions", "method": "POST", "action": "to-purchase-order", "title": "Purchase Order" },
      { "rel": "download", "href": "/invoices/3?format=PDF", "method": "GET", "title": "PDF" },
      { "rel": "download", "href": "/invoices/3?format=CSV", "method": "GET", "title": "CSV" },
      { "rel": "download", "href": "/invoices/3", "method": "GET", "content-type": "text/html", "title": "HTML" },
   ],
   {
      "id": 3,
      "state": "POSTED",
      "..."
   }
}