CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-backstage--catalog-model

Types and validation framework for Backstage's software catalog model with support for all entity kinds, relationships, and validation policies

93

1.05x

Evaluation93%

1.05x

Agent success when using this tile

Overview
Eval results
Files

entity-relations.mddocs/

Entity Relations

Typed relationships between catalog entities for dependency tracking, ownership, and organizational structure. The entity relations system provides a comprehensive framework for expressing and managing connections between different entities in the Backstage catalog.

Capabilities

Entity Relation Interface

The base structure for expressing relationships between catalog entities.

/**
 * A relation of a specific type to another entity in the catalog
 */
interface EntityRelation {
  /** The type of the relation (e.g., 'ownedBy', 'dependsOn') */
  type: string;
  /** The entity ref of the target of this relation */
  targetRef: string;
}

Ownership Relations

Relations expressing ownership and responsibility between entities and organizational units.

/**
 * Ownership relation constants
 */

/** 
 * An ownership relation where the owner is usually an organizational entity 
 * (user or group), and the other entity can be anything
 */
const RELATION_OWNED_BY = 'ownedBy';

/** 
 * A relationship from an owner to the owned entity 
 * Reversed direction of RELATION_OWNED_BY
 */
const RELATION_OWNER_OF = 'ownerOf';

Usage Examples:

import { 
  Entity, 
  ComponentEntity, 
  RELATION_OWNED_BY,
  RELATION_OWNER_OF 
} from "@backstage/catalog-model";

// Component owned by a team
const component: ComponentEntity = {
  apiVersion: "backstage.io/v1alpha1",
  kind: "Component",
  metadata: {
    name: "user-service"
  },
  spec: {
    type: "service",
    lifecycle: "production",
    owner: "team-backend"
  },
  relations: [
    {
      type: RELATION_OWNED_BY,
      targetRef: "group:default/team-backend"
    }
  ]
};

// Team that owns multiple components
const team: Entity = {
  apiVersion: "backstage.io/v1alpha1",
  kind: "Group",
  metadata: {
    name: "team-backend"
  },
  relations: [
    {
      type: RELATION_OWNER_OF,
      targetRef: "component:default/user-service"
    },
    {
      type: RELATION_OWNER_OF,
      targetRef: "component:default/order-service"
    }
  ]
};

API Relations

Relations expressing API provision and consumption between components and APIs.

/**
 * API relation constants
 */

/** A relation with an API entity, typically from a component */
const RELATION_CONSUMES_API = 'consumesApi';

/** A relation of an API being consumed, typically by a component */
const RELATION_API_CONSUMED_BY = 'apiConsumedBy';

/** A relation from an API provider entity (typically a component) to the API */
const RELATION_PROVIDES_API = 'providesApi';

/** A relation from an API to its provider entity (typically a component) */
const RELATION_API_PROVIDED_BY = 'apiProvidedBy';

Usage Examples:

import { 
  ComponentEntity, 
  ApiEntity,
  RELATION_PROVIDES_API,
  RELATION_API_PROVIDED_BY,
  RELATION_CONSUMES_API,
  RELATION_API_CONSUMED_BY
} from "@backstage/catalog-model";

// Component that provides an API
const apiProvider: ComponentEntity = {
  apiVersion: "backstage.io/v1alpha1",
  kind: "Component",
  metadata: { name: "user-service" },
  spec: {
    type: "service",
    lifecycle: "production",
    owner: "team-backend",
    providesApis: ["user-api"]
  },
  relations: [
    {
      type: RELATION_PROVIDES_API,
      targetRef: "api:default/user-api"
    }
  ]
};

// API provided by a component
const api: ApiEntity = {
  apiVersion: "backstage.io/v1alpha1",
  kind: "API",
  metadata: { name: "user-api" },
  spec: {
    type: "openapi",
    lifecycle: "production",
    owner: "team-backend",
    definition: "$text:./openapi.yaml"
  },
  relations: [
    {
      type: RELATION_API_PROVIDED_BY,
      targetRef: "component:default/user-service"
    }
  ]
};

// Component that consumes an API
const apiConsumer: ComponentEntity = {
  apiVersion: "backstage.io/v1alpha1",
  kind: "Component",
  metadata: { name: "frontend-app" },
  spec: {
    type: "website",
    lifecycle: "production",
    owner: "team-frontend",
    consumesApis: ["user-api"]
  },
  relations: [
    {
      type: RELATION_CONSUMES_API,
      targetRef: "api:default/user-api"
    }
  ]
};

Dependency Relations

Relations expressing dependencies and interdependencies between entities.

/**
 * Dependency relation constants
 */

/** A relation denoting a dependency on another entity */
const RELATION_DEPENDS_ON = 'dependsOn';

/** A relation denoting a reverse dependency by another entity */
const RELATION_DEPENDENCY_OF = 'dependencyOf';

Usage Examples:

import { 
  ComponentEntity, 
  ResourceEntity,
  RELATION_DEPENDS_ON,
  RELATION_DEPENDENCY_OF 
} from "@backstage/catalog-model";

// Component that depends on a database
const component: ComponentEntity = {
  apiVersion: "backstage.io/v1alpha1",
  kind: "Component",
  metadata: { name: "user-service" },
  spec: {
    type: "service",
    lifecycle: "production",
    owner: "team-backend",
    dependsOn: ["resource:default/user-database"]
  },
  relations: [
    {
      type: RELATION_DEPENDS_ON,
      targetRef: "resource:default/user-database"
    }
  ]
};

// Database resource that other components depend on
const database: ResourceEntity = {
  apiVersion: "backstage.io/v1alpha1",
  kind: "Resource",
  metadata: { name: "user-database" },
  spec: {
    type: "database",
    owner: "team-platform"
  },
  relations: [
    {
      type: RELATION_DEPENDENCY_OF,
      targetRef: "component:default/user-service"
    }
  ]
};

Hierarchical Relations

Relations expressing parent-child and part-whole relationships for organizational structure.

/**
 * Hierarchical relation constants
 */

/** A parent/child relation to build up a tree, used for organizational structure */
const RELATION_PARENT_OF = 'parentOf';

/** A relation from a child to a parent entity */
const RELATION_CHILD_OF = 'childOf';

/** A part/whole relation, typically for components in a system */
const RELATION_PART_OF = 'partOf';

/** A relation from a containing entity to a contained entity */
const RELATION_HAS_PART = 'hasPart';

Usage Examples:

import { 
  SystemEntity, 
  ComponentEntity,
  GroupEntity,
  RELATION_PART_OF,
  RELATION_HAS_PART,
  RELATION_PARENT_OF,
  RELATION_CHILD_OF 
} from "@backstage/catalog-model";

// System containing multiple components
const system: SystemEntity = {
  apiVersion: "backstage.io/v1alpha1",
  kind: "System",
  metadata: { name: "user-management" },
  spec: {
    owner: "team-backend"
  },
  relations: [
    {
      type: RELATION_HAS_PART,
      targetRef: "component:default/user-service"
    },
    {
      type: RELATION_HAS_PART,
      targetRef: "component:default/user-database"
    }
  ]
};

// Component that is part of a system
const component: ComponentEntity = {
  apiVersion: "backstage.io/v1alpha1",
  kind: "Component",
  metadata: { name: "user-service" },
  spec: {
    type: "service",
    lifecycle: "production",
    owner: "team-backend",
    system: "user-management"
  },
  relations: [
    {
      type: RELATION_PART_OF,
      targetRef: "system:default/user-management"
    }
  ]
};

// Group hierarchy
const parentGroup: GroupEntity = {
  apiVersion: "backstage.io/v1alpha1",
  kind: "Group",
  metadata: { name: "engineering" },
  spec: {
    type: "department",
    children: ["team-backend", "team-frontend"]
  },
  relations: [
    {
      type: RELATION_PARENT_OF,
      targetRef: "group:default/team-backend"
    },
    {
      type: RELATION_PARENT_OF,
      targetRef: "group:default/team-frontend"
    }
  ]
};

Membership Relations

Relations expressing group membership and organizational affiliation.

/**
 * Membership relation constants
 */

/** A membership relation, typically for users in a group */
const RELATION_MEMBER_OF = 'memberOf';

/** A relation from a group to its member, typically a user in a group */
const RELATION_HAS_MEMBER = 'hasMember';

Usage Examples:

import { 
  UserEntity, 
  GroupEntity,
  RELATION_MEMBER_OF,
  RELATION_HAS_MEMBER 
} from "@backstage/catalog-model";

// User that belongs to a team
const user: UserEntity = {
  apiVersion: "backstage.io/v1alpha1",
  kind: "User",
  metadata: { name: "john-doe" },
  spec: {
    profile: {
      displayName: "John Doe",
      email: "john.doe@company.com"
    },
    memberOf: ["team-backend"]
  },
  relations: [
    {
      type: RELATION_MEMBER_OF,
      targetRef: "group:default/team-backend"
    }
  ]
};

// Group with members
const group: GroupEntity = {
  apiVersion: "backstage.io/v1alpha1",
  kind: "Group",
  metadata: { name: "team-backend" },
  spec: {
    type: "team",
    members: ["john-doe", "jane-smith"],
    children: []
  },
  relations: [
    {
      type: RELATION_HAS_MEMBER,
      targetRef: "user:default/john-doe"
    },
    {
      type: RELATION_HAS_MEMBER,
      targetRef: "user:default/jane-smith"
    }
  ]
};

Relation Management Patterns

Bidirectional Relation Sync

import { 
  Entity, 
  EntityRelation,
  RELATION_DEPENDS_ON,
  RELATION_DEPENDENCY_OF 
} from "@backstage/catalog-model";

class RelationManager {
  /**
   * Ensure bidirectional relations are consistent
   */
  syncBidirectionalRelations(entities: Entity[]): Entity[] {
    const entityMap = new Map(entities.map(e => [this.getEntityKey(e), e]));
    
    entities.forEach(entity => {
      if (!entity.relations) return;
      
      entity.relations.forEach(relation => {
        const targetEntity = entityMap.get(relation.targetRef);
        if (!targetEntity) return;
        
        const reverseRelationType = this.getReverseRelationType(relation.type);
        if (!reverseRelationType) return;
        
        // Ensure reverse relation exists
        if (!targetEntity.relations) {
          targetEntity.relations = [];
        }
        
        const reverseRelation: EntityRelation = {
          type: reverseRelationType,
          targetRef: this.getEntityKey(entity)
        };
        
        // Add if not already present
        const hasReverseRelation = targetEntity.relations.some(r => 
          r.type === reverseRelation.type && r.targetRef === reverseRelation.targetRef
        );
        
        if (!hasReverseRelation) {
          targetEntity.relations.push(reverseRelation);
        }
      });
    });
    
    return entities;
  }
  
  private getEntityKey(entity: Entity): string {
    const namespace = entity.metadata.namespace || 'default';
    return `${entity.kind.toLowerCase()}:${namespace}/${entity.metadata.name}`;
  }
  
  private getReverseRelationType(relationType: string): string | null {
    const reversals: Record<string, string> = {
      [RELATION_DEPENDS_ON]: RELATION_DEPENDENCY_OF,
      [RELATION_DEPENDENCY_OF]: RELATION_DEPENDS_ON,
      'ownedBy': 'ownerOf',
      'ownerOf': 'ownedBy',
      'providesApi': 'apiProvidedBy',
      'apiProvidedBy': 'providesApi',
      'consumesApi': 'apiConsumedBy',
      'apiConsumedBy': 'consumesApi',
      'parentOf': 'childOf',
      'childOf': 'parentOf',
      'partOf': 'hasPart',
      'hasPart': 'partOf',
      'memberOf': 'hasMember',
      'hasMember': 'memberOf'
    };
    
    return reversals[relationType] || null;
  }
}

Relation Query System

import { Entity, EntityRelation } from "@backstage/catalog-model";

class RelationQueryBuilder {
  private entities: Entity[];
  
  constructor(entities: Entity[]) {
    this.entities = entities;
  }
  
  /**
   * Find all entities that have a specific relation to the target entity
   */
  findEntitiesWithRelationTo(targetEntityRef: string, relationType: string): Entity[] {
    return this.entities.filter(entity => {
      return entity.relations?.some(relation => 
        relation.type === relationType && relation.targetRef === targetEntityRef
      );
    });
  }
  
  /**
   * Find all entities that the source entity has a specific relation to
   */
  findEntitiesRelatedFrom(sourceEntity: Entity, relationType: string): Entity[] {
    if (!sourceEntity.relations) return [];
    
    const targetRefs = sourceEntity.relations
      .filter(relation => relation.type === relationType)
      .map(relation => relation.targetRef);
    
    return this.entities.filter(entity => {
      const entityRef = this.getEntityRef(entity);
      return targetRefs.includes(entityRef);
    });
  }
  
  /**
   * Build a dependency graph from entity relations
   */
  buildDependencyGraph(): Map<string, string[]> {
    const graph = new Map<string, string[]>();
    
    this.entities.forEach(entity => {
      const entityRef = this.getEntityRef(entity);
      const dependencies = entity.relations
        ?.filter(relation => relation.type === RELATION_DEPENDS_ON)
        .map(relation => relation.targetRef) || [];
      
      graph.set(entityRef, dependencies);
    });
    
    return graph;
  }
  
  /**
   * Find circular dependencies in the entity graph
   */
  findCircularDependencies(): string[][] {
    const graph = this.buildDependencyGraph();
    const visited = new Set<string>();
    const recursionStack = new Set<string>();
    const cycles: string[][] = [];
    
    const detectCycle = (node: string, path: string[]): void => {
      if (recursionStack.has(node)) {
        // Found cycle
        const cycleStart = path.indexOf(node);
        cycles.push(path.slice(cycleStart).concat([node]));
        return;
      }
      
      if (visited.has(node)) return;
      
      visited.add(node);
      recursionStack.add(node);
      
      const dependencies = graph.get(node) || [];
      dependencies.forEach(dep => {
        detectCycle(dep, [...path, node]);
      });
      
      recursionStack.delete(node);
    };
    
    graph.forEach((_, node) => {
      if (!visited.has(node)) {
        detectCycle(node, []);
      }
    });
    
    return cycles;
  }
  
  private getEntityRef(entity: Entity): string {
    const namespace = entity.metadata.namespace || 'default';
    return `${entity.kind.toLowerCase()}:${namespace}/${entity.metadata.name}`;
  }
}

// Usage
const queryBuilder = new RelationQueryBuilder(allEntities);

// Find all components that depend on a specific database
const dependentComponents = queryBuilder.findEntitiesWithRelationTo(
  "resource:default/user-database",
  RELATION_DEPENDS_ON
);

// Find all APIs provided by a component
const providedApis = queryBuilder.findEntitiesRelatedFrom(
  myComponent, 
  RELATION_PROVIDES_API
);

// Check for circular dependencies
const cycles = queryBuilder.findCircularDependencies();
if (cycles.length > 0) {
  console.warn("Circular dependencies detected:", cycles);
}

Relation Validation

import { Entity, parseEntityRef } from "@backstage/catalog-model";

class RelationValidator {
  private entities: Map<string, Entity>;
  
  constructor(entities: Entity[]) {
    this.entities = new Map(
      entities.map(e => [this.getEntityRef(e), e])
    );
  }
  
  /**
   * Validate that all entity relations point to existing entities
   */
  validateRelations(): Array<{entity: string, relation: string, error: string}> {
    const errors: Array<{entity: string, relation: string, error: string}> = [];
    
    this.entities.forEach((entity, entityRef) => {
      if (!entity.relations) return;
      
      entity.relations.forEach(relation => {
        // Check if target entity exists
        if (!this.entities.has(relation.targetRef)) {
          errors.push({
            entity: entityRef,
            relation: relation.targetRef,
            error: `Target entity does not exist: ${relation.targetRef}`
          });
          return;
        }
        
        // Validate relation type
        if (!this.isValidRelationType(relation.type)) {
          errors.push({
            entity: entityRef,
            relation: relation.targetRef,
            error: `Invalid relation type: ${relation.type}`
          });
        }
        
        // Validate relation target format
        try {
          parseEntityRef(relation.targetRef);
        } catch (error) {
          errors.push({
            entity: entityRef,
            relation: relation.targetRef,
            error: `Invalid target reference format: ${relation.targetRef}`
          });
        }
      });
    });
    
    return errors;
  }
  
  private isValidRelationType(type: string): boolean {
    const validTypes = [
      'ownedBy', 'ownerOf',
      'dependsOn', 'dependencyOf',
      'consumesApi', 'apiConsumedBy',
      'providesApi', 'apiProvidedBy',
      'parentOf', 'childOf',
      'partOf', 'hasPart',
      'memberOf', 'hasMember'
    ];
    
    return validTypes.includes(type);
  }
  
  private getEntityRef(entity: Entity): string {
    const namespace = entity.metadata.namespace || 'default';
    return `${entity.kind.toLowerCase()}:${namespace}/${entity.metadata.name}`;
  }
}

// Usage
const validator = new RelationValidator(allEntities);
const relationErrors = validator.validateRelations();

if (relationErrors.length > 0) {
  console.error("Relation validation errors:", relationErrors);
}

Install with Tessl CLI

npx tessl i tessl/npm-backstage--catalog-model

docs

core-entities.md

entity-kinds.md

entity-policies.md

entity-references.md

entity-relations.md

index.md

location-management.md

validation.md

tile.json