/**
* Relationship.js - RDF-based Relationship implementation for Ragno
*
* This class represents ragno:Relationship as a first-class RDF resource,
* following the ragno ontology specification where relationships are nodes
* rather than simple properties between entities.
*
* Key Features:
* - First-class RDF resource following ragno:Relationship
* - Source and target entity connections via ragno:hasSourceEntity/ragno:hasTargetEntity
* - Content and weight properties for relationship description
* - Full SKOS Concept compliance for semantic interoperability
* - Provenance tracking and metadata management
*/
import rdf from 'rdf-ext'
import RDFElement from './models/RDFElement.js'
import { logger } from '../Utils.js'
export default class Relationship extends RDFElement {
constructor(options = {}) {
// Initialize with relationship type
super({
...options,
type: 'relationship'
})
// Add ragno:Relationship type
this.addType(this.ns.classes.Relationship)
// Set source entity if provided
if (options.sourceEntity) {
this.setSourceEntity(options.sourceEntity)
}
// Set target entity if provided
if (options.targetEntity) {
this.setTargetEntity(options.targetEntity)
}
// Set description/content if provided
if (options.description) {
this.setContent(options.description)
}
// Set weight if provided
if (options.weight !== undefined) {
this.setWeight(options.weight)
}
// Set relationship type if provided
if (options.relationshipType) {
this.setRelationshipType(options.relationshipType)
}
logger.debug(`Created ragno:Relationship: ${this.uri}`)
}
/**
* Set the source entity for this relationship
* @param {RDFElement|NamedNode|string} sourceEntity - Source entity
*/
setSourceEntity(sourceEntity) {
this.removeTriple(this.ns.properties.hasSourceEntity)
const sourceNode = this._normalizeEntityReference(sourceEntity)
this.addTriple(this.ns.properties.hasSourceEntity, sourceNode)
logger.debug(`Set source entity: ${sourceNode.value} for relationship ${this.uri}`)
}
/**
* Get the source entity for this relationship
* @returns {NamedNode|null} Source entity node
*/
getSourceEntity() {
const quads = this.getTriplesWithPredicate(this.ns.properties.hasSourceEntity)
return quads.length > 0 ? quads[0].object : null
}
/**
* Set the target entity for this relationship
* @param {RDFElement|NamedNode|string} targetEntity - Target entity
*/
setTargetEntity(targetEntity) {
this.removeTriple(this.ns.properties.hasTargetEntity)
const targetNode = this._normalizeEntityReference(targetEntity)
this.addTriple(this.ns.properties.hasTargetEntity, targetNode)
logger.debug(`Set target entity: ${targetNode.value} for relationship ${this.uri}`)
}
/**
* Get the target entity for this relationship
* @returns {NamedNode|null} Target entity node
*/
getTargetEntity() {
const quads = this.getTriplesWithPredicate(this.ns.properties.hasTargetEntity)
return quads.length > 0 ? quads[0].object : null
}
/**
* Set the weight for this relationship
* @param {number} weight - Relationship weight
*/
setWeight(weight) {
this.removeTriple(this.ns.properties.hasWeight)
this.addTriple(this.ns.properties.hasWeight, rdf.literal(weight))
}
/**
* Get the weight for this relationship
* @returns {number|null} Relationship weight
*/
getWeight() {
const quads = this.getTriplesWithPredicate(this.ns.properties.hasWeight)
return quads.length > 0 ? parseFloat(quads[0].object.value) : null
}
/**
* Set the relationship type (semantic classification)
* @param {string} relType - Relationship type (e.g., "causal", "temporal", "part-of")
*/
setRelationshipType(relType) {
this.setSubType(relType)
}
/**
* Get the relationship type
* @returns {string|null} Relationship type
*/
getRelationshipType() {
return this.getSubType()
}
/**
* Create a bidirectional relationship (adds inverse)
* @param {string} [inverseDescription] - Description for inverse relationship
* @param {number} [inverseWeight] - Weight for inverse relationship
* @returns {Relationship} Inverse relationship
*/
createBidirectional(inverseDescription, inverseWeight) {
const sourceEntity = this.getSourceEntity()
const targetEntity = this.getTargetEntity()
if (!sourceEntity || !targetEntity) {
throw new Error('Cannot create bidirectional relationship without source and target entities')
}
const inverse = new Relationship({
dataset: this.dataset,
sourceEntity: targetEntity,
targetEntity: sourceEntity,
description: inverseDescription || this.getContent(),
weight: inverseWeight || this.getWeight(),
relationshipType: this.getRelationshipType()
})
// Link the relationships
this.connectTo(inverse.node)
inverse.connectTo(this.node)
return inverse
}
/**
* Check if this relationship connects two specific entities
* @param {RDFElement|NamedNode|string} entity1 - First entity
* @param {RDFElement|NamedNode|string} entity2 - Second entity
* @returns {boolean} True if relationship connects these entities
*/
connects(entity1, entity2) {
const node1 = this._normalizeEntityReference(entity1)
const node2 = this._normalizeEntityReference(entity2)
const source = this.getSourceEntity()
const target = this.getTargetEntity()
return (source && target && (
(source.equals(node1) && target.equals(node2)) ||
(source.equals(node2) && target.equals(node1))
))
}
/**
* Get the other entity in this relationship
* @param {RDFElement|NamedNode|string} entity - Known entity
* @returns {NamedNode|null} Other entity
*/
getOtherEntity(entity) {
const entityNode = this._normalizeEntityReference(entity)
const source = this.getSourceEntity()
const target = this.getTargetEntity()
if (source && source.equals(entityNode)) {
return target
} else if (target && target.equals(entityNode)) {
return source
}
return null
}
/**
* Check if this relationship involves a specific entity
* @param {RDFElement|NamedNode|string} entity - Entity to check
* @returns {boolean} True if entity is source or target
*/
involves(entity) {
const entityNode = this._normalizeEntityReference(entity)
const source = this.getSourceEntity()
const target = this.getTargetEntity()
return (source && source.equals(entityNode)) || (target && target.equals(entityNode))
}
/**
* Add evidence or support for this relationship
* @param {RDFElement|NamedNode|string} evidence - Evidence source (e.g., semantic unit)
*/
addEvidence(evidence) {
const evidenceNode = this._normalizeEntityReference(evidence)
this.addTriple(this.ns.provProperties.used, evidenceNode)
}
/**
* Get all evidence sources for this relationship
* @returns {Array<NamedNode>} Evidence nodes
*/
getEvidence() {
return this.getTriplesWithPredicate(this.ns.provProperties.used)
.map(quad => quad.object)
}
/**
* Normalize different entity reference formats to NamedNode
* @private
* @param {RDFElement|NamedNode|string} entity - Entity reference
* @returns {NamedNode} Normalized entity node
*/
_normalizeEntityReference(entity) {
if (typeof entity === 'string') {
return rdf.namedNode(entity)
} else if (entity && typeof entity === 'object' && entity.node) {
// RDFElement instance
return entity.node
} else if (entity && entity.termType === 'NamedNode') {
// Already a NamedNode
return entity
} else {
throw new Error(`Invalid entity reference: ${entity}`)
}
}
/**
* Validate this relationship according to ragno ontology
* @returns {Object} Validation result
*/
validate() {
const baseValidation = super.validate()
const errors = [...baseValidation.errors]
// Check ragno:Relationship type
if (!this.hasType(this.ns.classes.Relationship)) {
errors.push('Relationship must have ragno:Relationship type')
}
// Check required properties
if (!this.getSourceEntity()) {
errors.push('Relationship must have ragno:hasSourceEntity')
}
if (!this.getTargetEntity()) {
errors.push('Relationship must have ragno:hasTargetEntity')
}
// Check that source and target are different
const source = this.getSourceEntity()
const target = this.getTargetEntity()
if (source && target && source.equals(target)) {
errors.push('Relationship source and target must be different')
}
return {
valid: errors.length === 0,
errors
}
}
/**
* Get relationship metadata
* @returns {Object} Relationship metadata
*/
getMetadata() {
const baseMetadata = super.getMetadata()
return {
...baseMetadata,
sourceEntity: this.getSourceEntity()?.value,
targetEntity: this.getTargetEntity()?.value,
weight: this.getWeight(),
relationshipType: this.getRelationshipType(),
evidenceCount: this.getEvidence().length
}
}
/**
* Convert to simple object representation (for backwards compatibility)
* @returns {Object} Simple object representation
*/
toSimpleObject() {
return {
uri: this.uri,
description: this.getContent(),
source: this.getSourceEntity()?.value,
target: this.getTargetEntity()?.value,
weight: this.getWeight(),
type: this.getRelationshipType()
}
}
/**
* Create relationship from simple object (migration helper)
* @param {Object} obj - Simple object representation
* @param {Object} [options] - Additional options
* @returns {Relationship} RDF-based relationship
*/
static fromSimpleObject(obj, options = {}) {
return new Relationship({
...options,
sourceEntity: obj.source,
targetEntity: obj.target,
description: obj.description,
weight: obj.weight,
relationshipType: obj.type
})
}
/**
* Create a relationship between two entities with automatic naming
* @param {RDFElement|NamedNode|string} sourceEntity - Source entity
* @param {RDFElement|NamedNode|string} targetEntity - Target entity
* @param {string} [description] - Relationship description
* @param {Object} [options] - Additional options
* @returns {Relationship} Created relationship
*/
static create(sourceEntity, targetEntity, description, options = {}) {
return new Relationship({
...options,
sourceEntity,
targetEntity,
description
})
}
/**
* Clone this relationship with optional modifications
* @param {Object} [modifications] - Properties to modify in the clone
* @returns {Relationship} Cloned relationship
*/
clone(modifications = {}) {
const cloned = new Relationship({
dataset: rdf.dataset(), // New dataset for clone
sourceEntity: modifications.sourceEntity || this.getSourceEntity(),
targetEntity: modifications.targetEntity || this.getTargetEntity(),
description: modifications.description || this.getContent(),
weight: modifications.weight !== undefined ? modifications.weight : this.getWeight(),
relationshipType: modifications.relationshipType || this.getRelationshipType()
})
// Copy additional properties
for (const quad of this.getTriples()) {
if (!quad.predicate.equals(this.ns.properties.hasSourceEntity) &&
!quad.predicate.equals(this.ns.properties.hasTargetEntity) &&
!quad.predicate.equals(this.ns.properties.content) &&
!quad.predicate.equals(this.ns.properties.hasWeight) &&
!quad.predicate.equals(this.ns.dcProperties.created)) {
cloned.addTriple(quad.predicate, quad.object)
}
}
return cloned
}
}