Using collection to create resources vs adding members
tpluscode opened this issue · 6 comments
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 ?? ;
] ;
.
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?
What does it mean existing user representations ?
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 LINK
ed resource in fact does exist. Not a necessity though
I gave some thoughts and I see several ways of achieving it.
- 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.
- 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
orhydra: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 usehydra:expects rdf:Statement
where each reified statement would saycollection 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. - 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
Any news on this one @tpluscode ?
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
)