HydraCG / Specifications

Specifications created by the Hydra W3C Community Group
Other
138 stars 26 forks source link

Separating GET and PUT requests (a'la CQRS) #234

Open tpluscode opened 3 years ago

tpluscode commented 3 years ago

Describe your API requirement

In a recent project I have been implementing some resource endpoints which, in order to save requests, embed representations off linked resources. For example, a book and a cover could be separate resources

# /book/foo
</book/foo> a </api/Book> , schema:Book ; schema:image </image/foo> .

# /image/foo
</image/foo> a schema:ImageObject ; schema:contentUrl <https://s3...> .

In order to reduce the required roundtrips (think a collection of books + a request for every book's image) I would prefer to embed those images. Same for a singular GET /book/foo

The problem

The issue is, that when a client gets a book representation with image embedded, how do they request an update? How would they know that the schema:image should be omitted from a PUT request?

In the case of an embedded property, it could be annotated, for example with sh:ignoredProperty but this imposes limitations. How would I distinguish between embedded resource from a resource which is in fact integral part of the Book?

A similar case could be with inferenced types. The </api/Book> above could be subclass of schema:Book, so that the latter is not explicitly stored in the database about book instances. Thus, it also should not be part of the update request either, in order to prevent materialising of inferred triples.

Solution(s)

My current choice is to implement the client so that any time they wish to do an update, they would request a fresh instance using a Prefer header, such as Prefer: return=minimal or a custom preference

GET /book/foo
Prefer: return=minimal

This would instruct the server not to include any inferred triples nor embedded resources.

The problem I have with this idea is that the client needs to know to make this additional request first. The alternative could be to inject a marker flag into resources which came back with "extra bits" such as

</book/foo> api:representation api:Expanded

The server could guard against update which include a [] api:representation api:Expanded triple.

asbjornu commented 3 years ago

How about turning the Prefer solution around such that it matches what's being discussed in inadarei/draft-prefer-transclude#5? I.e.:

GET /book/foo HTTP/2.0
Prefer: transclude="image"

When a client explicitly has asked to transclude image it should also know image should not be a part of the subsequent request. No?

tpluscode commented 3 years ago

And if the client did not ask but server transcludes? Guess the client would look to Preference-applied header in both cases, correct?

Still does not exactly help address the inferencing case

asbjornu commented 3 years ago

Yes, I expect the server to respond with Preference-Applied as such:

HTTP/2.0 200 OK
Preference-Applied: transclude="image"

That will give the client enough information to not include image in a subsequent request, no?

tpluscode commented 3 years ago

It could but, as always, it depends. Does this preference support only one level transclusion?

And again, there is the case of inferred types etc, like "@id": [ "Child", "Parent" ]. Here the Parent would be implicitly added because <Child> rdfs:subClassOf <Parent> and thus an update request should not send it back

On top of all issues, this information would be provided transparently by the database so the application cannot even tell one from the other

asbjornu commented 3 years ago

Does this preference support only one level transclusion?

That depends on the query language supported by Prefer: transclude. This is the topic being discussed in inadarei/draft-prefer-transclude#5.

On top of all issues, this information would be provided transparently by the database so the application cannot even tell one from the other

I get your point. We probably need to annotate this inline and alongside the properties themselvese somehow. The concept of transclusion is old and I assume the linked nature of RDF has found solutions to this long before JSON-LD was invented? Aren't there any existing RDF solutions we can use here?

alien-mcl commented 3 years ago

What about some subclassing? Client receives a resource of type some:Class that has fully expanded set of statements. Client is aware of the possible operation that expects some:SpecialClass and that special class may be described with i.e. SHACL shape that would tell exactly what kind of properties are required to properly mint an instance of that some:SpecialClass. Those properties that are describing the recieved resource of type some:Class that are required by some:SpecialClass could be used (it will be probably a subset in your case).

tpluscode commented 3 years ago

I don't find this too practical. Yes, I actually plan on using SHACL but for any existing resource the client would have received, say,

# foaf:Agent inferred as superclass of foaf:Person
<John> a foaf:Person, foaf:Agent .

I would like the client to only send the explicit foaf:Person in a request to update <John>.

SHACL would be possible but that would require overly exact shapes, which would have to very precisely the allowed values of rdf:type to reject a request with foaf:Agent.

This will not work as a generalised solution. First, it prevents flexible schema evolution, which can add or remove inferred triples at any point in time. Second, each individual resource could potentially be different. <John> may only be explicit foaf:Person but another resource could in fact be stored as having both types (or additional types). Would I have a SHACL shape dedicated to specific resources rather than classes?

Finally, I would rather not require the clients to slice and dic the representations. What the GET, they should ideally PUT back without requiring them to work with hierarchies of shapes in order to mint and instance in order to update what the retrieved from the API

alien-mcl commented 3 years ago

Hmm - I though CQRS is about having different read and write models, but I assume you would like to hide that difference from the client and keep it to the server. While I think it's unusual (hypermedia is not about CRUD but about hypermedia that would drive the client), indeed it puts some additional stress on the client. In such a case, server should know what kind of statements could be inferred and remove them from the payload received from the client. Remember that client can use inference on it's side and no server side efforts can prevent it other than mentioned hypermedia.

asbjornu commented 3 years ago

I like the subclassing idea. Wouldn't it be possible to adjust a resource's @type by adding hydra:TranscludedResource or something similar, providing a hint that the resource is transcluded?

alien-mcl commented 3 years ago

I've found something that might be useful in this case:

https://www.w3.org/TR/dx-prof-conneg/#profilesinprof

It would be possible to describe alternate representations and their profiles from which the client could pick to retrieve.

tpluscode commented 3 years ago

Great! I think profile content negotiation is exactly what I'm looking for. Rather than using the return=minimal preference, which is intentionally vague, we could define a well-known identifier for a "canonical representation", which I would then implement as one which excludes any inferred, generated, or external (imported) knowledge about a resource.

Accept-Profile: <http://www.w3.org/ns/hydra/profile#Canonical>

As a more generalised feature, individual API-defined profiles could simply be SHACL Shapes, so that given multiple shapes which can describe a resource, they could be chosen by the client to request a specific shape of said resource.

# This shape has Person with given name + family name
<http://example.com/shape/PersonA>
  dash:applicableToClass schema:Person ;
  sh:property [ sh:path schema:givenName ] , [ sh:path schema:familyName ]
.

# This shape has Person with full name only
<http://example.com/shape/PersonB>
  dash:applicableToClass schema:Person ;
  sh:property [ sh:path schema:name ]
.
GET /person/John
Accept-Profile: <http://example.com/shape/PersonB>

We even discussed that the profile-shape could be an external resource which the server would dereference and dynamically create a query based on the sh:path values