HydraCG / Specifications

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

Using collection to create resources vs adding members #237

Closed tpluscode closed 3 years ago

tpluscode commented 3 years ago

Describe your API requirement

This a question not specifically related to Hydra as I have seen similar discussion for other systems too.

A typical scenario for using a collection resource, in our case an instance of hydra:Collection, is to do a POST request with the intended member representation

POST /users
Content-Type: text/turtle

prefix schema: <http://schema.org/>
<> a schema:Person ; schema:name "John Doe"

For the above request, the server could create a new identifier, for example by slugifying the name to produce a URL /user/john-doe.

The question is, how an API would expose the functionality of adding existing resources for the client to understand.

Solution(s)

I consider implementing a generic supported operation handler, which would work slightly differently depending on the operation's definition.

Base operation

The core would be just a POST, expecting api:User as payload

api:UserCollection
  hydra:supportedOperation [
    hydra:method "POST" ;
    hydra:expects api:User ;
  ] ;
.

Operation to create users

api:UserCollection
  hydra:supportedOperation [
+   rdf:type schema:CreateAction ;
    hydra:method "POST" ;
    hydra:expects api:User ;
  ] ;
.

By adding the schema:CreateAction type, the client would be informed that the operation creates new users. A request containing <> a api:User or [] a api:User would have these resources created with a server-defined URI as mentioned above

Operation to add resources to collection

Now the fun part: How to change an operation so that the client and the server know that they should send existing resources. And what would the request look like?

api:UserCollection
  hydra:supportedOperation [
+   rdf:type schema:UpdateAction ;
    hydra:method "POST" ;
-   hydra:expects api:User ;
+   hydra:expects ?? ;
  ] ;
.

By typing the operation as schema:UpdateAction but not the other, the server would only allow payloads with existing user representations. It should be enough to send just the identifiers, right? To add two members to collection, could that be either

<user/John> a api:User .
<user/Jane> a api:User .

or, using a different expects:

POST <users>

<users> hydra:member <user/John> ; <user/Jane> .

Having it both ways?

Given what I've just realised when writing the previous section, two alternative operations could be necessary to have a single collection resource allow both kinds of requests. Combined:

api:UserCollection
  hydra:supportedOperation [
    rdf:type schema:CreateAction ;
    hydra:method "POST" ;
    hydra:expects api:User ;
  ] , [
    rdf:type schema:UpdateAction ;
    hydra:method "POST" ;
    hydra:expects ?? ;
  ] ;
.
tpluscode commented 3 years ago

Aha, an the elephant in the room: Removing elements from collection :D

Maybe LINK and UNLINK method could be used while otherwise keeping similar payloads as above?

alien-mcl commented 3 years ago

What does it mean existing user representations ?

tpluscode commented 3 years ago

Guess that should be rephrased simply as existing users, as in having URI identifiers.

Given the proposed request with only the ids, I agree they do not even need to "exist" in the sense of having actual representations.

On the other hand, I'd expect a typical API to be slightly closed world, so that such request would in fact verify that a LINKed resource in fact does exist. Not a necessity though

alien-mcl commented 3 years ago

I gave some thoughts and I see several ways of achieving it.

  1. PUT whole collection (or at least list of member IRIs) - the most primitive brute force approach. In case of small collections doable, but when number of members grows it becomes unusable due to size of payload. Not recommended.
  2. POST/LINK/UNLINK for creating brand new resources within the collection/attaching/detaching an existing one. In this case I'd go with hydra:expects xsd:anyUri or hydra:expects rdf:Resource. The former seems ugly as it forces the client to send a barrow of literals (I intentionally didn't use a bag word), the latter feels so generic as it is vague on what server will do with the provided statements (as this is the only way for RDF) while it needs just IRIs. As an alternative I'd use hydra:expects rdf:Statement where each reified statement would say collection is-in-relation-with member, where that relation actually doesn't matter. I don't know other ways of saying give me an IRI of a resource - it's all I care about.
  3. PATCH - hybrid between the two above, but this put's to the client in a poor situation as it would need to know on how to mint the body of a request. What would it be - set of SPIN statements? - a nightmare.

In all cases, I'd recommend using schema actions to denote an intent of the operation as the pure protocol may be not enough

alien-mcl commented 3 years ago

Any news on this one @tpluscode ?

tpluscode commented 3 years ago

This is a design question and itself it probably does not have one answer.

With human audience in mind I would probably go for a diff-like payload, which would simply list members to add and members to remove. The exact specifics are irrelevant as long as the server provides a precise description of such a payload and it is enough for a (human) client to figure out.

The request could be a PATCH with changes described using the Changeset vocabulary

prefix change: <http://purl.org/vocab/changeset/schema#>

# remove /member/1
# add    /member/2
[
  change:removal [
    rdf:subject </collection> ;
    rdf:predicate hydra:member ;
    rdf:object </member/1> ;
  ] ;
  change:addition [
    rdf:subject </collection> ;
    rdf:predicate hydra:member ;
    rdf:object </member/2> ;
  ] 
]

The downside is that each collection would need a specific expects, for example done with SHACL

<> a sh:NodeShape ;
  sh:property [
    sh:name "Remove collection member" ;
    sh:path change:removal ; 
    sh:node [
      sh:property [
        sh:path rdf:subject ;
        sh:hasValue </collection> ;
        dash:hidden true ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
      ], [
        sh:path rdf:predicate ;
        sh:hasValue hydra:member ;
        dash:hidden true ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
      ], [
        sh:path rdf:object ;
      ]
    ]
  ] , 
  sh:property [
    sh:name "Add collection member" ;
    sh:path change:addition ; 
    sh:node [
      sh:property [
        sh:path rdf:subject ;
        sh:hasValue </collection> ;
        dash:hidden true ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
      ], [
        sh:path rdf:predicate ;
        sh:hasValue hydra:member ;
        dash:hidden true ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
      ], [
        sh:path rdf:object ;
      ]
    ]
  ] ;

With the rdf:subject and rdf:object pinned to specific values, hidden, and restricted to exactly one value, all the UI would need is to provide UI for selecting members to add/remove (rdf:object)