mirror of
https://codeberg.org/fediverse/fep.git
synced 2026-08-05 11:46:04 +00:00
(FEP-5bf0): Rework API to use SHACL (#93)
This updates the proposal again, this time leveraging SHACL in order to represent more complex possibilities. Co-authored-by: Michael Puckett <michaelcpuckett@gmail.com> Reviewed-on: https://codeberg.org/fediverse/fep/pulls/93 Co-authored-by: michaelcpuckett <michaelcpuckett@noreply.codeberg.org> Co-committed-by: michaelcpuckett <michaelcpuckett@noreply.codeberg.org>
This commit is contained in:
committed by
David Sterry
parent
fbff3b8cbc
commit
eaae974fbf
+207
-83
@@ -8,19 +8,9 @@ dateReceived: 2023-04-10
|
||||
|
||||
## Summary
|
||||
|
||||
This proposal would allow Collections to have a `streams` property, as Actors do. The streams would be additional Collections that are sorted or filtered versions of the original Collection.
|
||||
This proposal would allow Collections to have a `streams` property, as Actors do. The streams would be of the type CollectionView, a proposed vocabulary extension that represents a sorted and/or filtered version of a Collection. ActivityPub clients could then render CollectionViews without having to perform such filtering or sorting operations themselves.
|
||||
|
||||
Additionally, metadata about the sorting/filtering could be indicated using new proposed additions to the ActivityStreams vocabulary: `orderedBy`, `reversed`, `filteredProperty`, and `filteredValue`.
|
||||
|
||||
## Implementation
|
||||
|
||||
Sorted Collections would always be of the type `Collection` so as not to violate the purpose of OrderedCollections, but would use `orderedItems` instead of `items`. The `reversed` property would indicate that a sorted Collection is in the reverse order.
|
||||
|
||||
The `filteredProperty` and `filteredValue` attributes would indicate the specific property being filtered and its corresponding value. These attributes can be represented as strings that reference properties from the Activity Vocabulary (e.g., `type` and `Person`).
|
||||
|
||||
When multiple filters are applied, both `filteredProperty` and `filteredValue` would be arrays of strings.
|
||||
|
||||
When applying filters to properties within nested objects, dot notation would identify the targeted property for filtering (e.g., `object.type`).
|
||||
Metadata about how the sorting or filtering has been applied would be applied using new proposed vocabulary extensions that leverage SHACL for describing constraints.
|
||||
|
||||
## Motivations
|
||||
|
||||
@@ -30,104 +20,238 @@ Currently, in order to support filtering or sorting, these clients need to retri
|
||||
|
||||
This proposal would allow servers to perform these kinds of operations, either at runtime or ahead of time, to ease the burden on clients.
|
||||
|
||||
Regarding S2S protocols: other servers should be free to explore the Collections, but they can be easily ignored, along with the new properties.
|
||||
Other servers should be free to explore the Collections, but they can be easily ignored, along with the new properties.
|
||||
|
||||
## Implementation
|
||||
|
||||
A CollectionView extends from OrderedCollection and represents a filtered and/or sorted version of a Collection. Similarly, a CollectionViewPage extends from OrderedCollectionPage.
|
||||
|
||||
The method of filtering applied to the CollectionView can be indicated via its "filter" property, which maps to one or more SHACL Shapes.
|
||||
|
||||
The vocabulary would also provide a SHACL Shape for indicating that a property's value is among the items in a particular ActivityStreams Collection.
|
||||
|
||||
The method of sorting applied to the CollectionView can be indicated via its "sort" property, which maps to a SortShape. A SortShape extends SHACL's PropertyShape and adds the "order" property which can be mapped to "Ascending" or "Descending".
|
||||
|
||||
If there is no "sort" property, the order is the same as that of the original Collection.
|
||||
|
||||
## Examples
|
||||
|
||||
### Sorting
|
||||
Here, an Actor's Inbox returns all Activities posted by the Actor, and the server also provides filtered versions as streams for client consumption.
|
||||
|
||||
Hashtags will typically be entered into a database according to the order of creation, however it is more useful for them to be displayed alphabetically.
|
||||
The first CollectionView returns only the Like Activities.
|
||||
|
||||
The second CollectionView returns only created Articles with replies, demonstrating multiple filters and filtering on nested properties.
|
||||
|
||||
The third CollectionView demonstrates how to indicates that a given property's value is in a partiular ActivityStreams Collection. In the example, the CollectionView is returning Activities by Alyssa's Co-workers. (Alyssa has a custom stream of mutual followers who she has labeled as Co-workers.)
|
||||
|
||||
```json
|
||||
{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"id": "https://example.social/hashtags",
|
||||
"url": "https://example.social/hashtags",
|
||||
"name": "Hashtags",
|
||||
"type": "Collection",
|
||||
"totalItems": 100,
|
||||
"first": "https://example.social/hashtags/page/1",
|
||||
"last": "https://example.social/hashtags/page/2",
|
||||
"streams": [
|
||||
{
|
||||
"id": "https://example.social/hashtags/ordered/name",
|
||||
"url": "https://example.social/hashtags/ordered/name",
|
||||
"name": "Hashtags",
|
||||
"summary": "Hashtags, Ordered by Name (A -> Z)",
|
||||
"type": "Collection",
|
||||
"totalItems": 100,
|
||||
"first": "https://example.social/hashtags/ordered/name/page/1",
|
||||
"last": "https://example.social/hashtags/ordered/name/page/2",
|
||||
"orderedBy": "name"
|
||||
},
|
||||
{
|
||||
"id": "https://example.social/hashtags/ordered/name/reversed",
|
||||
"url": "https://example.social/hashtags/ordered/name/reversed",
|
||||
"name": "Hashtags",
|
||||
"summary": "Hashtags, Ordered by Name (Z -> A)",
|
||||
"type": "Collection",
|
||||
"totalItems": 100,
|
||||
"first": "https://example.social/hashtags/ordered/name/reversed/page/1",
|
||||
"last": "https://example.social/hashtags/ordered/name/reversed/page/2",
|
||||
"orderedBy": "name",
|
||||
"reversed": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Filtering
|
||||
|
||||
Here, an Actor's Inbox returns all Activities posted by the Actor, and the server also provides filtered versions for client consumption.
|
||||
|
||||
The first filtered stream returns only the Like Activities.
|
||||
|
||||
The second filtered stream filters Activities by their type and their nested Object's type to return only created Notes.
|
||||
|
||||
```json
|
||||
{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"@context": {
|
||||
"@vocab": "https://www.w3.org/ns/activitystreams#",
|
||||
"fep": "https://w3id.org/fep#",
|
||||
"CollectionView": "fep:CollectionView",
|
||||
"viewOf": "fep:viewOf",
|
||||
"filter": "fep:filter",
|
||||
"sort": "fep:sort",
|
||||
"inCollection": "fep:inCollection",
|
||||
"SortShape": "fep:SortShape",
|
||||
"order": "fep:order",
|
||||
"sh": "http://www.w3.org/ns/shacl#",
|
||||
"PropertyShape": "sh:PropertyShape",
|
||||
"path": "sh:path",
|
||||
"hasValue": "sh:hasValue",
|
||||
"minCount": "sh:minCount"
|
||||
},
|
||||
"id": "https://example.social/@alyssa/inbox",
|
||||
"url": "https://example.social/@alyssa/inbox",
|
||||
"name": "Inbox",
|
||||
"type": "OrderedCollection",
|
||||
"name": "Inbox",
|
||||
"totalItems": 1000,
|
||||
"first": "https://example.social/@alyssa/inbox/page/1",
|
||||
"last": "https://example.social/@alyssa/inbox/page/2",
|
||||
"streams": [
|
||||
{
|
||||
"id": "https://example.social/@alyssa/inbox/likes",
|
||||
"url": "https://example.social/@alyssa/inbox/likes",
|
||||
"type": "CollectionView",
|
||||
"name": "Likes",
|
||||
"summary": "Incoming Likes to Alyssa's Inbox",
|
||||
"type": "OrderedCollection",
|
||||
"totalItems": 100,
|
||||
"filter": {
|
||||
"type": "PropertyShape",
|
||||
"path": "type",
|
||||
"hasValue": "Like"
|
||||
},
|
||||
"sort": {
|
||||
"type": "SortShape",
|
||||
"path": "published",
|
||||
"order": "Descending"
|
||||
},
|
||||
"totalItems": 10,
|
||||
"first": "https://example.social/@alyssa/inbox/likes/page/1",
|
||||
"last": "https://example.social/@alyssa/inbox/likes/page/2",
|
||||
"filteredProperty": "type",
|
||||
"filteredValue": "Like"
|
||||
"last": "https://example.social/@alyssa/inbox/likes/page/1"
|
||||
},
|
||||
{
|
||||
"id": "https://example.social/@alyssa/inbox/notes",
|
||||
"url": "https://example.social/@alyssa/inbox/notes",
|
||||
"name": "Notes",
|
||||
"summary": "Incoming Notes to Alyssa's Inbox",
|
||||
"type": "OrderedCollection",
|
||||
"totalItems": 100,
|
||||
"first": "https://example.social/@alyssa/inbox/notes/page/1",
|
||||
"last": "https://example.social/@alyssa/inbox/notes/page/2",
|
||||
"filteredProperty": ["type", "object.type"],
|
||||
"filteredValue": ["Create", "Note"]
|
||||
"id": "https://example.social/@alyssa/inbox/posts-with-replies",
|
||||
"type": "CollectionView",
|
||||
"name": "Posts with Replies",
|
||||
"filter": [
|
||||
{
|
||||
"type": "PropertyShape",
|
||||
"path": "type",
|
||||
"hasValue": "Create"
|
||||
},
|
||||
{
|
||||
"type": "PropertyShape",
|
||||
"path": ["object", "inReplyTo"],
|
||||
"minCount": 1
|
||||
}
|
||||
],
|
||||
"sort": {
|
||||
"type": "SortShape",
|
||||
"path": "published",
|
||||
"order": "Descending"
|
||||
},
|
||||
"totalItems": 10,
|
||||
"first": "https://example.social/@alyssa/inbox/blog-posts/page/1",
|
||||
"last": "https://example.social/@alyssa/inbox/blog-posts/page/1",
|
||||
"viewOf": "https://example.social/@alyssa/inbox"
|
||||
},
|
||||
{
|
||||
"id": "https://example.social/@alyssa/inbox/notes-by-coworkers",
|
||||
"type": "CollectionView",
|
||||
"name": "Posts by Co-Workers",
|
||||
"filter": {
|
||||
"type": "InCollectionShape",
|
||||
"path": "actor",
|
||||
"inCollection": "https://example.social/@alyssa/friends/coworkers"
|
||||
},
|
||||
"sort": {
|
||||
"type": "SortShape",
|
||||
"path": "published",
|
||||
"order": "Descending"
|
||||
},
|
||||
"totalItems": 10,
|
||||
"first": "https://example.social/@alyssa/inbox/notes-by-coworkers/page/1",
|
||||
"last": "https://example.social/@alyssa/inbox/notes-by-coworkers/page/1",
|
||||
"viewOf": "https://example.social/@alyssa/inbox"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Note that this example using an Actor's Inbox may be more suitably linked via an Actor's `streams` instead of on the Inbox itself but is designed to show how filters might work.
|
||||
## Vocabulary Extensions
|
||||
|
||||
Here are the terms that would needed to be added to the FEP vocabulary:
|
||||
|
||||
```json
|
||||
{
|
||||
"@context": {
|
||||
"fep": "https://w3id.org/fep#",
|
||||
"as": "https://www.w3.org/ns/activitystreams#",
|
||||
"sh": "http://www.w3.org/ns/shacl#",
|
||||
"rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
|
||||
"rdfs": "http://www.w3.org/2000/01/rdf-schema#"
|
||||
},
|
||||
"fep:CollectionView": {
|
||||
"@id": "fep:CollectionView",
|
||||
"@type": "rdfs:Class",
|
||||
"rdfs:subClassOf": "as:OrderedCollection",
|
||||
"rdfs:label": "Collection View",
|
||||
"rdfs:comment": "Represents a sorted and/or filtered version of a Collection"
|
||||
},
|
||||
"fep:CollectionViewPage": {
|
||||
"@id": "fep:CollectionViewPage",
|
||||
"@type": "rdfs:Class",
|
||||
"rdfs:subClassOf": "as:OrderedCollectionPage",
|
||||
"rdfs:label": "Collection View Page",
|
||||
"rdfs:comment": "Represents a sorted and/or filtered version of a CollectionPage"
|
||||
},
|
||||
"fep:SortShape": {
|
||||
"@id": "fep:SortShape",
|
||||
"@type": "rdfs:Class",
|
||||
"rdfs:subClassOf": "sh:PropertyShape",
|
||||
"rdfs:label": "Sort Shape",
|
||||
"rdfs:comment": "A PropertyShape with an 'order' property"
|
||||
},
|
||||
"fep:filter": {
|
||||
"@id": "fep:filter",
|
||||
"@type": "rdf:Property",
|
||||
"rdfs:domain": "fep:CollectionView",
|
||||
"rdfs:range": "sh:Shape",
|
||||
"rdfs:label": "Filter",
|
||||
"rdfs:comment": "The method of filtering applied to the CollectionView"
|
||||
},
|
||||
"fep:sort": {
|
||||
"@id": "fep:sort",
|
||||
"@type": "rdf:Property",
|
||||
"rdfs:domain": "fep:CollectionView",
|
||||
"rdfs:range": "fep:SortShape",
|
||||
"rdfs:label": "Sort",
|
||||
"rdfs:comment": "The method of sorting applied to the CollectionView"
|
||||
},
|
||||
"fep:order": {
|
||||
"@id": "fep:order",
|
||||
"@type": "rdf:Property",
|
||||
"rdfs:domain": "fep:SortShape",
|
||||
"rdfs:range": "fep:SortOrderType",
|
||||
"rdfs:label": "Order",
|
||||
"rdfs:comment": "Indicates whether the sort order is 'Ascending' or 'Descending'"
|
||||
},
|
||||
"fep:SortOrderType": {
|
||||
"@id": "fep:SortOrderType",
|
||||
"@type": "rdfs:Class",
|
||||
"rdfs:label": "Sort Order Type",
|
||||
"rdfs:comment": "For indicating the sort order"
|
||||
},
|
||||
"fep:Ascending": {
|
||||
"@id": "fep:Ascending",
|
||||
"@type": "fep:SortOrderType",
|
||||
"rdfs:label": "Ascending",
|
||||
"rdfs:comment": "Indicates ascending sort order"
|
||||
},
|
||||
"fep:Descending": {
|
||||
"@id": "fep:Descending",
|
||||
"@type": "fep:SortOrderType",
|
||||
"rdfs:label": "Descending",
|
||||
"rdfs:comment": "Indicates descending sort order"
|
||||
},
|
||||
"fep:InCollectionShape": {
|
||||
"@id": "fep:InCollectionShape",
|
||||
"@type": "rdfs:Class",
|
||||
"rdfs:subClassOf": "sh:PropertyShape",
|
||||
"rdfs:label": "In Collection Shape",
|
||||
"rdfs:comment": "For filtering on whether a property's value is in a Collection.",
|
||||
"sh:js": "fep:inCollectionFunction"
|
||||
},
|
||||
"fep:inCollection": {
|
||||
"@id": "fep:inCollection",
|
||||
"@type": "rdf:Property",
|
||||
"rdfs:domain": "fep:InCollectionShape",
|
||||
"rdfs:range": "sh:IRI",
|
||||
"rdfs:label": "In Collection",
|
||||
"rdfs:comment": "Maps a Collection URL to fep:inCollectionFunction"
|
||||
},
|
||||
"fep:inCollectionFunction": {
|
||||
"@id": "fep:inCollectionFunction",
|
||||
"@type": "sh:JSFunction",
|
||||
"sh:jsFunction": "inCollection",
|
||||
"sh:jsLibrary": "https://w3id.org/fep/functions.js",
|
||||
"sh:parameter": {
|
||||
"@type": "sh:Parameter",
|
||||
"sh:path": "fep:inCollection"
|
||||
}
|
||||
},
|
||||
"fep:viewOf": {
|
||||
"@id": "fep:viewOf",
|
||||
"@type": "rdf:Property",
|
||||
"rdfs:label": "View of",
|
||||
"rdfs:domain": "fep:CollectionView",
|
||||
"rdfs:range": "as:Collection",
|
||||
"rdfs:comment": "A reference back to the original Collection"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
Servers could in theory make available a templated URL endpoint that allows for arbitrary sorting or filtering. This should be discouraged, as it could lead to database injections. Instead, only predetermined sorted/filtered Collections should be made available via the `streams` property.
|
||||
Servers could in theory make available a templated URL endpoint that allows for arbitrary sorting or filtering. This should be discouraged, as it could lead to database injections. Instead, only predetermined sorted/filtered CollectionViews should be made available via the `streams` property.
|
||||
|
||||
## References
|
||||
|
||||
|
||||
Reference in New Issue
Block a user