RDF-star inspired module for Zotonic enables reification of edges as resources to enable discussion and metadata about relationships.
In Zotonic, edges represent relationships between resources (e.g., "Article A" → "author" → "Person B"). Sometimes you want to talk about these relationships dispute them, verify them, comment on them, or add metadata.
This module allows you to "reify" edges turn them into resources themselves. Once reified, you can:
- 💬 Comment on relationships
⚠️ Dispute or verify connections- 📝 Add metadata to edges
- 🔗 Create edges pointing to other edges (meta-relationships)
This is inspired by RDF-star (RDF 1.2)
- Add to your Zotonic site's
rebar.configdependencies:
{deps, [
{zotonic_mod_edge_star, {git, "https://github.com/channelme/zotonic_mod_edge_star.git", {branch, "main"}}}
]}.- Run:
make- Enable the module in your site's admin interface (
/admin/modules) or via config:
{modules, [
...
mod_edge_star
]}. - The module will automatically create the
edge_stardatabase table on first run.
% Create a regular edge
{ok, EdgeId} = m_edge:insert(ArticleId, author, PersonId, Context),
% Later, someone disputes it - reify the edge
{ok, EdgeRscId} = m_edge_star:reify(EdgeId, Context),
% Now create a normal edge pointing to the reified edge
{ok, _} = m_edge:insert(UserId, disputes, EdgeRscId, Context).You can also work with triples directly:
% Insert using a triple notation
{ok, EdgeId} = m_edge_star:insert(
UserId,
disputes,
{ArticleId, author, PersonId}, % Triple
Context
).
% This automatically:
% 1. Finds or creates the edge (ArticleId → author → PersonId)
% 2. Reifies it as a resource
% 3. Creates the dispute edge (UserId → disputes → ReifiedEdge)Reify an edge - create a resource that represents it.
-spec reify(EdgeId :: integer(), Context :: z:context()) ->
{ok, RscId :: integer()} | {error, Reason :: term()}.Example:
{ok, EdgeRscId} = m_edge_star:reify(123, Context).If the edge is already reified, returns the existing resource ID. The reified resource will:
- Have category
edge_resource - Have a title like "Subject → predicate → Object"
- Include edge details in the body
- Be assigned to the
system_content_group
Insert an edge, with automatic reification if needed.
-spec insert(Subject, Predicate, Object, Context) ->
{ok, EdgeId :: integer()} | {error, Reason :: term()}.
Subject :: integer(),
Predicate :: atom(),
Object :: integer() | triple() | {edge, integer()},
Context :: z:context().Examples:
% Regular edge
{ok, EdgeId} = m_edge_star:insert(User, likes, Article, Context).
% Edge to a triple (auto-reifies)
{ok, EdgeId} = m_edge_star:insert(
User,
disputes,
{Article, author, Person}, % Triple gets reified
Context
).
% Edge to an edge ID (auto-reifies)
{ok, EdgeId} = m_edge_star:insert(
Admin,
verified,
{edge, 456}, % Edge 456 gets reified
Context
).
% With options
{ok, EdgeId} = m_edge_star:insert(User, disputes, Triple, [{seq, 100}], Context).Get the resource ID for a reified edge.
-spec get_rsc_id(EdgeId :: integer() | triple(), Context :: z:context()) ->
integer() | undefined.Example:
case m_edge_star:get_rsc_id(EdgeId, Context) of
undefined ->
io:format("Edge not reified~n");
RscId ->
io:format("Edge reified as resource ~p~n", [RscId])
end.Get the original edge ID from a reified resource.
-spec get_edge_id(RscId :: integer(), Context :: z:context()) ->
integer() | undefined.Example:
OriginalEdgeId = m_edge_star:get_edge_id(ReifiedRscId, Context).Query subjects or objects related to a triple.
Subjects = m_edge_star:subjects({Article, author, Person}, disputes, Context).
Objects = m_edge_star:objects(UserId, disputes, Context).Get edge IDs with automatic triple expansion.
% Returns list of {ResourceId | Triple, EdgeId} tuples
EdgeIds = m_edge_star: object_edge_ids(UserId, disputes, Context).
% Example: [{123, 456}, {{789, author, 101}, 457}]{% with m. edge. id[article_id]. author[person_id] as edge_id %}
{% with m.edge_star.rsc_id[edge_id] as edge_rsc %}
{% if edge_rsc %}
{# Show who disputes this relationship #}
{% for user_id in m.edge. subjects[edge_rsc].disputes %}
<div class="alert alert-warning">
{{ m.rsc[user_id].title }} disputes this authorship
</div>
{% endfor %}
{# Show who verified it #}
{% for user_id in m.edge.subjects[edge_rsc].verified %}
<span class="badge badge-success">
✓ Verified by {{ m.rsc[user_id].title }}
</span>
{% endfor %}
{% endif %}
{% endwith %}
{% endwith %}{% wire id="dispute-btn"
postback={dispute_edge edge_id=edge_id}
delegate="your_module" %}
<button id="dispute-btn" class="btn btn-warning">
Dispute this connection
</button>{# Get reified resource ID from edge ID #}
{{ m.edge_star.rsc_id[edge_id] }}
{# Check subjects of a reified edge #}
{% for id in m.edge_star.s[article_id][author][person_id]. disputes %}
...
{% endfor %}
{# Check objects #}
{% for id in m. edge_star.o[user_id]. disputes %}
...
{% endfor %}% User tags article as spam
{ok, EdgeId} = m_edge: insert(ArticleId, subject, SpamCategoryId, Context),
% Moderator flags it as incorrect
{ok, _} = m_edge_star: insert(ModeratorId, flags_incorrect, {edge, EdgeId}, Context).% Someone claims ownership
{ok, EdgeId} = m_edge:insert(UserId, owns, AccountId, Context),
% Admin verifies the claim
{ok, _} = m_edge_star:insert(AdminId, verified, {edge, EdgeId}, Context).% Paper cites another paper
{ok, EdgeId} = m_edge:insert(PaperId, cites, OtherPaperId, Context),
% Researcher disputes the citation context
{ok, _} = m_edge_star:insert(ResearcherId, disputes_context, {edge, EdgeId}, Context).% AI suggests a relationship
{ok, EdgeId} = m_edge:insert(PersonId, born_in, CityId, [{created_by, ai_system}], Context),
% Human reviewer verifies it
{ok, _} = m_edge_star:insert(ReviewerId, verified, {edge, EdgeId}, Context).The module creates a simple edge_star table:
CREATE TABLE edge_star (
edge_id INTEGER PRIMARY KEY REFERENCES edge(id) ON DELETE CASCADE,
rsc_id INTEGER NOT NULL UNIQUE REFERENCES rsc(id) ON DELETE CASCADE,
created TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);One-to-one relationship: Each edge can have at most one reified resource.
This module is inspired by RDF-star but implements a pragmatic version:
- ✅ Edges as objects: You can create edges pointing to (reified) edges
- ✅ Quoted triples: Work with
{Subject, Predicate, Object}tuples - ⏸️ RDF 1.2 alignment: Currently follows the limitation that edges can only be objects, not subjects of other quoted triples
- 🔮 Future extension: The design allows for later supporting edges as subjects
- Caching:
get_rsc_id/2andget_edge_id/2usez_depcachewith 1-hour TTL - Indexes: Automatic indexes on primary key and unique constraint
- Transactions: Reification uses database transactions for atomicity
- Lazy reification: Edges are only reified when needed, not automatically
Regular Edge:
Article --[author]--> Person
Reified Edge (after dispute):
Article --[author]--> Person
↑
|
[Edge Resource]
↑
|
User --[disputes]--
The reified edge becomes a first-class resource that can:
- Have its own edges (incoming and outgoing)
- Have comments via
mod_comment - Have access control via normal Zotonic ACL
- Appear in search results
- Have media attachments
make testmake dialyzerContributions welcome! Please:
- Fork the repository
- Create a feature branch
- Make your changes with tests
- Submit a pull request
Apache License 2.0
Maas-Maarten Zeeman (@mmzeeman)
- Issues: GitHub Issues
- Zotonic Community: Zotonic Discussions