Types and validation framework for Backstage's software catalog model with support for all entity kinds, relationships, and validation policies
93
Evaluation — 93%
↑ 1.05xAgent success when using this tile
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.
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;
}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"
}
]
};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"
}
]
};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"
}
]
};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"
}
]
};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"
}
]
};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;
}
}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);
}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-modeldocs
evals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10