"""Machine and Asset relationship models.""" from shopdb.extensions import db from .base import BaseModel class RelationshipType(BaseModel): """ Types of relationships between assets. ADR-001 seeds three canonical types: partof, controls, connectedto. Sites may add legacy/communication-flavored types (Serial Cable, Direct Ethernet, USB, WiFi, Dualpath) for backward compatibility with pre-1.0 data, but new ADR-001 code paths only reason about the three canonical types via free-text label for nuance. """ __tablename__ = 'relationshiptypes' relationshiptypeid = db.Column(db.Integer, primary_key=True) relationshiptype = db.Column(db.String(50), unique=True, nullable=False) description = db.Column(db.Text) color = db.Column(db.String(20), comment='CSS color for relationship badges') # True: edge has a source->target meaning (controls, partof, Backup For). # False: symmetric link (Dualpath, connectedto, USB...) shown on the card # once per peer with no direction, both stored direction rows collapsed. isdirectional = db.Column( db.Boolean, default=True, nullable=False, server_default='1', comment='False = symmetric connection, shown direction-blind' ) # Sibling propagation (ADR-001), WIRED. When a relationship of this type is # created, the create path fans it out across every through-type in this # type's propagation set (relationshiptypepropagations rows). For each # SYMMETRIC through-type P, the source's edge to the target is mirrored to # every asset linked to the target via P. This is M:N so one type can # propagate through several rails at once. Seeded rows on this DB: # controls -> partof (declared; directional rail, NOT consumed yet - # parent/child fan-out direction is ambiguous) # controls -> Dualpath (consumed; a dual-bay pair is one physical machine # with one controller, so both bays carry controls) # A dual-bay Dualpath pair thus gets the PC's controls link on both bays. # propagatesthrough is the list of through-type RelationshipType rows. propagatesthrough = db.relationship( 'RelationshipType', secondary='relationshiptypepropagations', primaryjoin='RelationshipType.relationshiptypeid' ' == RelationshipTypePropagation.relationshiptypeid', secondaryjoin='RelationshipType.relationshiptypeid' ' == RelationshipTypePropagation.throughtypeid', viewonly=True, ) def __repr__(self): return f"" class RelationshipTypePropagation(db.Model): """ M:N propagation rails (ADR-001). One row means: a relationship of type relationshiptypeid propagates through the connections of type throughtypeid. Replaces the old single-valued relationshiptypes.propagatesthroughid so a type (controls) can propagate through several through-types (partof AND Dualpath) at once. Seed/CLI-managed; no management UI. """ __tablename__ = 'relationshiptypepropagations' propagationid = db.Column(db.Integer, primary_key=True) relationshiptypeid = db.Column( db.Integer, db.ForeignKey('relationshiptypes.relationshiptypeid'), nullable=False ) throughtypeid = db.Column( db.Integer, db.ForeignKey('relationshiptypes.relationshiptypeid'), nullable=False ) __table_args__ = ( db.UniqueConstraint( 'relationshiptypeid', 'throughtypeid', name='uq_reltype_propagation' ), ) def __repr__(self): return f"" class AssetRelationship(BaseModel): """ Relationships between assets. Examples: - Computer controls Machine - Two machines are dualpath partners - Network device connects to machine """ __tablename__ = 'assetrelationships' relationshipid = db.Column(db.Integer, primary_key=True) sourceassetid = db.Column( db.Integer, db.ForeignKey('assets.assetid'), nullable=False ) targetassetid = db.Column( db.Integer, db.ForeignKey('assets.assetid'), nullable=False ) relationshiptypeid = db.Column( db.Integer, db.ForeignKey('relationshiptypes.relationshiptypeid'), nullable=False ) # Free-text description carrying domain nuance ("DNC feed", # "operator workstation", "ethernet PoE"). Avoids inflating type list. label = db.Column(db.String(200), comment='Free-text relationship description (ADR-001)') # When true, resolve_asset_position walks across this edge (priority 2 # in the resolution chain). Defaults to true for partof + controls when # the relationship is created via the API; nullable for legacy rows. inheritsposition = db.Column( db.Boolean, default=True, nullable=False, server_default='1', comment='If true, resolved-position walk follows this edge (ADR-001)' ) notes = db.Column(db.Text) sourceasset = db.relationship( 'Asset', foreign_keys=[sourceassetid], backref='outgoing_relationships' ) targetasset = db.relationship( 'Asset', foreign_keys=[targetassetid], backref='incoming_relationships' ) relationshiptype = db.relationship('RelationshipType', backref='asset_relationships') __table_args__ = ( db.UniqueConstraint( 'sourceassetid', 'targetassetid', 'relationshiptypeid', name='uq_asset_relationship' ), db.Index('idx_asset_rel_source', 'sourceassetid'), db.Index('idx_asset_rel_target', 'targetassetid'), ) def __repr__(self): return f" {self.targetassetid}>"