REST API should support composite (atomic) operations
When creating a list, it is desirable that it either exist correctly and completely configured, or not exist at all.
Similarly, when subscribing a user, the user's preferences (including and especially delivery_status) should be atomic.
And it should be possible to create a list with an initial set of mandatory subscribers, also atomically.
The REST API doesn't provide for this, since creating a list, configuring its attributes/properties, adding the owner & initial subscriber, their preferences, domain... each require multiple REST calls. This means that the list is visible in an incomplete state. (I counted ~10 API calls in my client - not counting loops (e.g. for each subscriber)).
Some of this could be solved by allowing all attributes to be specified on the POST call to create a list. But that doesn't address the issue of also subscribing an initial group of users, which involves different objects.
To solve this within the constraints of a REST interface, the API should be extended to allow a single request to contain multiple operations, which are treated as a single transaction by the database. This can probably be done without tearing up the current api by serializing the current sequence of operations into a single POST.
E.g. Roughly (don't take this literally)
POST( '/transaction', [
if_not_exists => { user => "new-list-owner",
do => [ "POST" => { user => "new-list-owner", email => me@example.net, },
"PATCH" => { user_id => %post.result, moderation_action => 'accept', .. } ]
}
if_not_exists => { list => my-new-list,
do => [ POST => { list => {fqdn_listnmae =, description -,},
PATCH => { archive_policy => 'public', ...} ...]
],
get => { lists => my-new-list, items => [ 'config', 'subscribers' ] }
)
The POST would return something like
[ "status": "success",
"actions": [
{ "status": "Predicate not met", "result":{} },
{ "status": "OK", "result": {json result},
...]
More formally (but still rough), the semantics of a post to /transaction would be:
- Do the following in a single database transaction; everything succeeds or the transaction is rolled-back.
- The argument is an array of (predicated) actions and their arguments. The actions are the current API functions
- Some macro substitution/interpolation is necessary to chain operations (e.g. a newly created user to set prefs/subscribe)
- Results of each action are added to a JSON array; the same result as if the action were an independent REST call
- Predicates are things like: If this object exists (or does not); resulting status must be one of (...); 'always' (default)
- An action succeeds iff (the underlying action is successful) or (the predicate wasn't met, skipping the action) or (the failure result of the underlying action is allowed by the predicate)
- Failure of an action fails (and rolls-back) the transaction.
- Success of all actions commits the transaction
Internally, you'd open a DB transaction, take the sequence apart and dispatch each as a virtual 'post', 'patch', ...; merge the results, and return the merges result. If any non-conditional action in the sequence fails, rollback the transaction.
This should require just a thin wrapper around the existing API service...just not nesting the DB transaction that wraps each API function.
It would need to be worked out more carefully, but seems like a reasonable starting point, at least conceptually.
There is some additional discussion of this on mailman3-users and (I think) in other issues...