#
2 audited services
#
Audited Services
Stack Overflow allows collaborative editing. Let's walk through an example where Alice creates a new question and Bob edits the text. Since all audit features are on, both versions of the question are retained and can be fetched through services.
#
Create
Alice POSTs a new question to /api/question on December 31.
{
"title": "example title",
"body": "example body"
}
She gets back 201 Created and a location header Location: http://.../api/question/1.
She can GET http://.../api/question/1 to get the full body.
{
"id": 1,
"title": "example title",
"body": "example body",
"systemFrom": "2017-12-31T23:59:59.000Z",
"systemTo": null,
"createdById": "Alice",
"createdOn": "2017-12-31T23:59:59.000Z",
"lastUpdatedById": "Alice",
"answers": [],
"version": {
"number": 1,
"systemFrom": "2017-12-31T23:59:59.000Z",
"systemTo": null,
"createdById": "Alice",
"createdOn": "2017-12-31T23:59:59.000Z",
"lastUpdatedById": "Alice"
}
}
idwas set to 1. The next created question will be 2. The next Answer will be 1. Each type gets its own sequence.systemFromwas set to December 31.systemTois null, indicating that there is no phase-out time; that this data is currently active.createdOnmatchessystemFromfor now.createdByIdmatcheslastUpdatedByIdfor now.- The audit properties appear duplicated on the version. This explained later in composite writes.
#
Update
Bob PUTs a new version to /api/question/1 the next day, on January 1.
{
"title": "edited title",
"body": "edited body",
"version": {
"number": 1
}
}
The version number is present and matches, so the edit succeeds. He gets back 204 No Content.
He can GET http://.../api/question/1 to get the full body.
{
"id": 1,
"title": "edited title",
"body": "edited body",
"systemFrom": "2018-01-01T23:59:59.000Z",
"systemTo": null,
"createdById": "Alice",
"createdOn": "2017-12-31T23:59:59.000Z",
"lastUpdatedById": "Bob",
"answers": [],
"version": {
"number": 2,
"systemFrom": "2018-01-01T23:59:59.000Z",
"systemTo": null,
"createdById": "Alice",
"createdOn": "2017-12-31T23:59:59.000Z",
"lastUpdatedById": "Bob"
}
}
systemFromwas set to January 1.systemTois null again.- Version 1 (which we'll look at next) had its
systemToupdated to the same time, January 1. - The version number was updated to 2.
lastUpdatedByIdwas updated to Bob.createdByIdandcreatedOnare unchanged.
#
Read by version
To read old versions, we can enhance the read service with an optional version parameter and an optional criterion.
service QuestionResource
{
getById(id: Long[1..1]): QuestionReadProjection[0..1]
{
operation : read;
url : /question/{id: Long[1..1]}?{version: Integer[0..1] version};
criteria : this.id == id;
optionalCriteria: this.version.number == version
format : json;
}
}
Leaving off the version query parameter would give the latest version. But now we can GET http://.../api/question/1?version=1 to see the previous version.
{
"id": 1,
"title": "example title",
"body": "example body",
"systemFrom": "2017-12-31T23:59:59.000Z",
"systemTo": "2018-01-01T23:59:59.000Z",
"createdById": "Alice",
"createdOn": "2017-12-31T23:59:59.000Z",
"lastUpdatedById": "Alice",
"answers": [],
"version": {
"number": 1,
"systemFrom": "2017-12-31T23:59:59.000Z",
"systemTo": "2018-01-01T23:59:59.000Z",
"createdById": "Alice",
"createdOn": "2017-12-31T23:59:59.000Z",
"lastUpdatedById": "Alice"
}
}
Everything is identical as when Alice first created it, except for systemTo. It started as null and was changed to January 1, the time version 2 was created.
systemTemporal turns the data store into an immutable append-only log. The only in-place updates are to systemTo and the only possible edit is to phase out a version (by changing systemTo from null to the current instant) while simultaneously phasing in a new version at the same instant. Or, in the case of a delete, not phasing in any new version.
#
Read by time
While it's usually more convenient to deal with version numbers, we can also query for old versions by time.
service Question
{
getById(id: Long[1..1]): QuestionReadProjection[0..1]
{
operation : read;
url : /question/{id: Long[1..1]}?{version: Integer[0..1] version}&{system: TemporalRange[0..1] system};
criteria : this.id == id;
optionalCriteria: this.version.number == version
optionalCriteria: this.system == system
format : json;
}
}
Now we can GET http://.../api/question/1?system=2018-01-01T00:00:00.000Z to see the previous version.
Time ranges are inclusive on the from end and exclusive on the to end. So any time within the range [2017-12-31T23:59:59.000Z, 2018-01-01T23:59:59.000Z) would return the first version.
The time range check is fairly complex at the data layer. If we're querying for the current version (the system parameter is not provided or is null) then we have to check that systemTo is also null. Otherwise we have to check that the system parameter falls between systemFrom and systemTo, also taking into account the fact that systemTo may be null. We could have defined the additional criteria like this:
this.systemTo == null && system == null
|| system != null
&& this.systemFrom <= system
&& (this.systemTo == null || system < this.systemTo)
The synthetic property system exists to simplify criteria like this, and to use in association criteria as described in Audit Model. Thus we're able to simplify the optionalCriteria to just this.system == system.