This document describes the RESTful API with CRUD functionality for Current Storage.
TODO: Describe storage field <-> endpoint bindings.
The response body is always either a valid JSON or empty with 204 (No content)
error code.
The generic format of a successful JSON response is:
{
"url": "https://api.example.com/...",
"url_*": "https://api.example.com/...",
... more "url_*" fields ...
[optional]
"meta": {
...
},
[required]
"data": {
...
}
}
The responses are strongly typed. The contents of the request URL define the schema for the response.
The value for "data"
can be:
- an object,
- an array, if the response is expected to contain a list,
- primitive type (string, number, boolean) in special cases.
Error responses are JSON objects with the top-level "error":...
field:
{
“error”: “Detailed human-readable error message.”,
... other fields ...
}
Successful responses are JSON objects without the top-level "error"
field.
TODO: We also may have redirects, which contain the "Location" in the header, with empty body.
The default way to encode JSON objects is JavaScript-friendly. The API also supports F#-friendly format.
The differences are:
-
Variant types.
If a field
x
can be either anapple
or anorange
, the JavaScript-friendly representation is{...,"x":{"apple":{...},...}
, and the F#-friendly representation is{...,"x":{"Case":"apple","Fields":[...]},...}}
. -
Coding of absent optional objects.
The JavaScript-friendly implementation omits missing fields from the output (and does not expect them in the input). The F#-friendly implementation outputs missing fields as
null
-s (and expectsnull
-s in the input). -
Coding of dictionaries with the keys being strings.
Generally, both JavaScript- and F#-friendly implementations encode dictionaries as arrays of arrays of two elements:
[[key1,value1],[key2,value2],...]
.However, if the type of
key
is string, the JavaScript-friendly format of this dictionary becomes an object:{"key1":value1,"key2":value2,...}
.TODO(dkorolev): Check if some other F#-related issues should be added into this section.
View is a representation of a record, which contains some non-empty subset of the record’s fields.
The API supports different views via the &format=...
URL parameter.
By default, records returned by the API contain all fields.
Two implicitly defined views (both default to "all fields") are full
and preview
.
The full
view is the default, so that &format=full
is equivalent to not specifying the &format
URL parameter.
The &format=preview
is used when the response schema for certain endpoint contains not one record, but a collection of them. In other words, the representation of each record of "data":[...]
for the endpoints where "data"
is an array is equivalent to the representation of those records with &format=preview
.
The views are defined by the user, and the default representation of any view is "all fields". In particular, unless the user defines the "preview"
view, it will be identical to the "full"
one.
Current Storage API interprets HTTP verbs in the following way:
-
GET: Get the resource.
Strongly typed with respect to returning an individual record or a collection of records. For example,
/users
can be the way to GET the list of users ("data":[...]
), and/user/1
can be the way to GET an individual user with the ID of1
. -
PATCH: Update some fields of the resource.
PATCH
should be called on the URL that has been (or could have been) returned as the top-level"url":"..."
field of the payload ofGET
.Generally, the user is expected to retrieve the value of the object under the key
"data"
of the top-level payload of aGET
request, alter this object, and send its modified version back to the server.PATCH
respects the format of the"data"
controlled by the&format=
URL parameter. Sending aPATCH
request to the URL with certain&format=X
can only modify the fields returned by theGET
request of this&format=X
.For
PATCH
requests, the key of the record being updated is taken from the URL. The user is expected to either not send in the key as part of the payload, or ensure the key contained in the payload matches the key contained in the URL.PATCH
will fail if attempted to modify a non-existing resource, resulting in404 (Not Found)
error code.Also,
PATCH
performs conflict resolution. The URL beingPATCH
-ed must contain a revision / timestamp of the resource. The exact semantics of the revision / timestamp is not specified by this RFC, but the top-level"url"
field returned by theGET
request is guaranteed to have it set.An attempt to
PATCH
to a URL that points to the version of the resource other than the most recent one will result in no data mutation and a409 Conflict
error code. The user is then expected to re-run theGET
request, followed by the re-runPATCH
request onto the URL of the most recent version of the resource returned by theGET
request. Confirming that the modifications that occurred in the meantime do not conflict with the new payload being sent in the body of thePATCH
request is then the responsibility of the API user. -
PUT: Create a new record for a resource, or update all fields of an existing resource.
Attempting to
PUT
to a URL that has an incomplete view of a record would result in a405 Method Not Allowed
error.In the
&format=full
view,PUT
is equivalent toPATCH
.PUT
behaves the same way asPATCH
when it comes to avoiding merge conflicts.When legal, the only difference between
PUT
andPATCH
is thatPUT
will create a new resource if it did not exist before the call. A201 Created
response code will then be returned. UsePATCH
if this behavior is undesirable.TODO: Location header https://tools.ietf.org/html/rfc7231#section-4.3.3
-
POST: Create a new record and assign it to some resourse.
POST
will create a new resource. The URL for POST must not contain the key(s) for the record being posted, and the payload body should not contain the key(s) either.The generation of the new key is user-defined. Three common strategies are: 1) random, 2) the [salted] hash of the payload, 3) the value of certain passed in field or fields.
In the latter two, as well as other, cases, a
POST
request may result in an attempt to overwrite an already existing resource. By default,POST
will not overwrite, but return a409 Conflict
error code, with the top-level"url"
field of the returned JSON object containing the URL for the resource that was about to be overwritten.The
&overwrite=1
/&overwrite=true
URL parameter can be set to indicate that thePOST
request should complete successfully even if it would result in overwriting existing data.
Successful POST
results in either 200 (OK)
error code if the resource has been updated, or 201 (Created)
.
-
DELETE: Deletes the resource.
Similarly to
PATCH
andPUT
,DELETE
should be called on the URL that has been (or could have been) returned as the top-level"url":"..."
field of the payload ofGET
.DELETE
follows the same conflict resolution rules asPATCH
andPUT
.
TODO: User-defined data integrity checks, and what errors are returned if they fail?
The entry point (usually, "/"
) URL will contain the list of inner "url_*"
-s to access respective fields ("tables") of the storage.
The entry point URL may also contain the URLs by which the user can access the respective schema files. This is applicable to F# and C++.
[ TO DISCUSS ] The payload when GET
-ting the resource corresponding to an individual record (ex. /user/123
) will contain the {...,"url_collection":".../users"}
reference to the URL to browse the whole collection, the record from which is being requested. Each resource can belong to one and only one collection.
As a record is successfully created or modified, the response payload will contain its URL as the top-level "url"
field.
When a resource can not be found, the error payload will contain the URL to browse the respective collection.
When the operation fails due to a conflict, the top-level "url"
field will be set as well.
TODO: Revisit this section - discuss stateful/stateless approaches.
When retrieving the collection of records, they are returned in pages. If there are more records to browse, the top-level response object will contain the "url_next_page"
field, as well as the "url_previous_page"
one for the pages past the first one.
As mentioned above, in the pagination mode:
- the
"data":[...]
top-level field becomes an array, not object, and - the elements of this array default to the
&format=preview
format, if defined by the user.
With respect to pagination:
- The user SHOULD NOT attempt to skip through records (ex. browse page 1 with records 1..20, and then skip immediately to records 101..120.
- The user CAN expect the records they have already seen as part of this session will not change.
- The user SHOULD NOT expect any record they have seen or may see will exist and/or be available to them by the time the user decides to perform actions on that record.
In other words, the behavior of pagination for the user mimics the architecture where on the first GET
request to browse the collection, the complete list of possible IDs is populated on the server, and the user is just browsing through these IDs.
The server will honor the entries the user has already browsed through, even if the user requests the previous page, and some entries from it have been deleted. At the same time, while browsing further, the server may re-fetch the records and re-apply the filters.
In particular, scanning through pages CAN return records created AFTER the time of the first GET
request, unless the explicit filter "created before X" has been requested by the user.
The token returned by the API to page through the collection expires by itself. The default period for which the token will be live is 10 minutes since it was last used.
TODO: Document page size and the ability to dynamically change it.