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
Utilities for tracking entity sources and managing location-based metadata and references. The location management system enables Backstage to track where catalog entities originate from and manage the relationship between entities and their source locations.
Standard annotation constants for tracking entity location information throughout the catalog system.
/**
* Standard location annotation constants
*/
/** Annotation for the location that manages this entity */
const ANNOTATION_LOCATION = 'backstage.io/managed-by-location';
/** Annotation for the original location where this entity was first discovered */
const ANNOTATION_ORIGIN_LOCATION = 'backstage.io/managed-by-origin-location';
/** Annotation for the current source location of this entity */
const ANNOTATION_SOURCE_LOCATION = 'backstage.io/source-location';Usage Examples:
import {
Entity,
ANNOTATION_LOCATION,
ANNOTATION_SOURCE_LOCATION
} from "@backstage/catalog-model";
const entityWithLocation: Entity = {
apiVersion: "backstage.io/v1alpha1",
kind: "Component",
metadata: {
name: "user-service",
annotations: {
[ANNOTATION_LOCATION]: "url:https://github.com/myorg/services/blob/main/user-service/catalog-info.yaml",
[ANNOTATION_SOURCE_LOCATION]: "url:https://github.com/myorg/services/tree/main/user-service/"
}
},
spec: {
type: "service",
lifecycle: "production",
owner: "team-backend"
}
};Parse location reference strings into structured components for processing and validation.
/**
* Parse a location reference string into its component parts
* @param ref - Location reference string in format "type:target"
* @returns Object with type and target components
*/
function parseLocationRef(ref: string): { type: string; target: string };Usage Examples:
import { parseLocationRef } from "@backstage/catalog-model";
// Parse URL location
const urlLocation = parseLocationRef("url:https://github.com/myorg/repo/blob/main/catalog-info.yaml");
// Result: { type: "url", target: "https://github.com/myorg/repo/blob/main/catalog-info.yaml" }
// Parse file location
const fileLocation = parseLocationRef("file:/tmp/catalog-info.yaml");
// Result: { type: "file", target: "/tmp/catalog-info.yaml" }
// Parse custom location type
const customLocation = parseLocationRef("gitlab:project/123/file/catalog-info.yaml");
// Result: { type: "gitlab", target: "project/123/file/catalog-info.yaml" }Convert location reference objects back into canonical string representations.
/**
* Convert a location reference object into a canonical string representation
* @param ref - Location reference object with type and target
* @returns Canonical string representation of the location reference
*/
function stringifyLocationRef(ref: { type: string; target: string }): string;Usage Examples:
import { stringifyLocationRef } from "@backstage/catalog-model";
// Stringify location reference
const locationRef = { type: "url", target: "https://example.com/catalog-info.yaml" };
const refString = stringifyLocationRef(locationRef);
// Result: "url:https://example.com/catalog-info.yaml"
// Round-trip conversion
const originalRef = "file:/home/user/catalog-info.yaml";
const parsed = parseLocationRef(originalRef);
const reconstructed = stringifyLocationRef(parsed);
// reconstructed === originalRefExtract source location information from entity annotations for tracking and management purposes.
/**
* Get the source location of an entity from its annotations
* @param entity - Entity to extract source location from
* @returns Location reference object with type and target
*/
function getEntitySourceLocation(entity: Entity): { type: string; target: string };Usage Examples:
import {
Entity,
getEntitySourceLocation,
ANNOTATION_SOURCE_LOCATION
} from "@backstage/catalog-model";
const entity: Entity = {
apiVersion: "backstage.io/v1alpha1",
kind: "Component",
metadata: {
name: "user-service",
annotations: {
[ANNOTATION_SOURCE_LOCATION]: "url:https://github.com/myorg/services/tree/main/user-service/"
}
}
};
const sourceLocation = getEntitySourceLocation(entity);
// Result: { type: "url", target: "https://github.com/myorg/services/tree/main/user-service/" }
// Handle entities without source location
try {
const location = getEntitySourceLocation(entityWithoutLocation);
} catch (error) {
console.log("Entity has no source location annotation");
}import {
Entity,
parseLocationRef,
getEntitySourceLocation,
ANNOTATION_LOCATION
} from "@backstage/catalog-model";
class LocationAwareProcessor {
async processEntity(entity: Entity): Promise<Entity> {
try {
const sourceLocation = getEntitySourceLocation(entity);
// Different processing based on location type
switch (sourceLocation.type) {
case 'url':
return this.processUrlBasedEntity(entity, sourceLocation.target);
case 'file':
return this.processFileBasedEntity(entity, sourceLocation.target);
default:
return this.processGenericEntity(entity);
}
} catch (error) {
// Entity has no source location
return this.processGenericEntity(entity);
}
}
private async processUrlBasedEntity(entity: Entity, url: string): Promise<Entity> {
// Add URL-specific metadata
return {
...entity,
metadata: {
...entity.metadata,
annotations: {
...entity.metadata.annotations,
'backstage.io/source-url': url,
'backstage.io/last-updated': new Date().toISOString()
}
}
};
}
private async processFileBasedEntity(entity: Entity, filePath: string): Promise<Entity> {
// Add file-specific metadata
return {
...entity,
metadata: {
...entity.metadata,
annotations: {
...entity.metadata.annotations,
'backstage.io/source-file': filePath
}
}
};
}
private processGenericEntity(entity: Entity): Entity {
return entity;
}
}import {
parseLocationRef,
stringifyLocationRef
} from "@backstage/catalog-model";
class LocationValidator {
private readonly allowedTypes = ['url', 'file', 'gitlab', 'github'];
validateLocationRef(ref: string): boolean {
try {
const parsed = parseLocationRef(ref);
return this.isValidLocationType(parsed.type) &&
this.isValidTarget(parsed.type, parsed.target);
} catch (error) {
return false;
}
}
normalizeLocationRef(ref: string): string {
const parsed = parseLocationRef(ref);
// Normalize based on type
switch (parsed.type) {
case 'url':
return stringifyLocationRef({
type: 'url',
target: this.normalizeUrl(parsed.target)
});
case 'file':
return stringifyLocationRef({
type: 'file',
target: this.normalizeFilePath(parsed.target)
});
default:
return ref;
}
}
private isValidLocationType(type: string): boolean {
return this.allowedTypes.includes(type);
}
private isValidTarget(type: string, target: string): boolean {
switch (type) {
case 'url':
return this.isValidUrl(target);
case 'file':
return this.isValidFilePath(target);
default:
return target.length > 0;
}
}
private normalizeUrl(url: string): string {
// Remove trailing slashes, ensure proper protocol
return url.replace(/\/+$/, '');
}
private normalizeFilePath(path: string): string {
// Normalize file path separators, resolve relative paths
return path.replace(/\\/g, '/');
}
private isValidUrl(url: string): boolean {
try {
new URL(url);
return true;
} catch {
return false;
}
}
private isValidFilePath(path: string): boolean {
return path.length > 0 && !path.includes('..') && !path.includes('//');
}
}
// Usage
const validator = new LocationValidator();
const isValid = validator.validateLocationRef("url:https://example.com/catalog.yaml");
const normalized = validator.normalizeLocationRef("url:https://example.com/catalog.yaml/");import {
Entity,
parseLocationRef,
ANNOTATION_LOCATION
} from "@backstage/catalog-model";
interface LocationDiscoveryResult {
entities: Entity[];
location: string;
errors: string[];
}
class EntityLocationDiscovery {
async discoverEntitiesAtLocation(locationRef: string): Promise<LocationDiscoveryResult> {
const location = parseLocationRef(locationRef);
const result: LocationDiscoveryResult = {
entities: [],
location: locationRef,
errors: []
};
try {
switch (location.type) {
case 'url':
return await this.discoverFromUrl(location.target, result);
case 'file':
return await this.discoverFromFile(location.target, result);
default:
result.errors.push(`Unsupported location type: ${location.type}`);
return result;
}
} catch (error) {
result.errors.push(`Discovery failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
return result;
}
}
private async discoverFromUrl(url: string, result: LocationDiscoveryResult): Promise<LocationDiscoveryResult> {
// Fetch and parse entities from URL
const response = await fetch(url);
const content = await response.text();
// Parse YAML/JSON content to find entities
const entities = this.parseEntitiesFromContent(content);
// Add location annotations to discovered entities
result.entities = entities.map(entity => ({
...entity,
metadata: {
...entity.metadata,
annotations: {
...entity.metadata.annotations,
[ANNOTATION_LOCATION]: `url:${url}`
}
}
}));
return result;
}
private async discoverFromFile(filePath: string, result: LocationDiscoveryResult): Promise<LocationDiscoveryResult> {
// Read and parse entities from file
// Implementation would read file system
result.errors.push("File-based discovery not implemented");
return result;
}
private parseEntitiesFromContent(content: string): Entity[] {
// Parse YAML/JSON content to extract entities
// This is a simplified implementation
try {
const parsed = JSON.parse(content);
return Array.isArray(parsed) ? parsed : [parsed];
} catch {
// Try YAML parsing if JSON fails
return [];
}
}
}
// Usage
const discovery = new EntityLocationDiscovery();
const result = await discovery.discoverEntitiesAtLocation(
"url:https://github.com/myorg/catalog/blob/main/entities.yaml"
);
console.log(`Discovered ${result.entities.length} entities`);
if (result.errors.length > 0) {
console.error("Discovery errors:", result.errors);
}import {
Entity,
getEntitySourceLocation,
parseLocationRef
} from "@backstage/catalog-model";
class LocationBasedRelationships {
findRelatedEntitiesByLocation(entities: Entity[], targetEntity: Entity): Entity[] {
try {
const targetLocation = getEntitySourceLocation(targetEntity);
const targetRepo = this.extractRepositoryFromLocation(targetLocation);
return entities.filter(entity => {
try {
const entityLocation = getEntitySourceLocation(entity);
const entityRepo = this.extractRepositoryFromLocation(entityLocation);
return entityRepo === targetRepo && entity !== targetEntity;
} catch {
return false;
}
});
} catch {
return [];
}
}
private extractRepositoryFromLocation(location: { type: string; target: string }): string | null {
if (location.type === 'url') {
// Extract repository from GitHub/GitLab URLs
const match = location.target.match(/github\.com\/([^\/]+\/[^\/]+)/);
return match ? match[1] : null;
}
return null;
}
groupEntitiesByLocation(entities: Entity[]): Map<string, Entity[]> {
const locationGroups = new Map<string, Entity[]>();
entities.forEach(entity => {
try {
const location = getEntitySourceLocation(entity);
const locationKey = `${location.type}:${this.extractRepositoryFromLocation(location) || location.target}`;
if (!locationGroups.has(locationKey)) {
locationGroups.set(locationKey, []);
}
locationGroups.get(locationKey)!.push(entity);
} catch {
// Entity has no source location
const noLocationKey = 'unknown:no-location';
if (!locationGroups.has(noLocationKey)) {
locationGroups.set(noLocationKey, []);
}
locationGroups.get(noLocationKey)!.push(entity);
}
});
return locationGroups;
}
}
// Usage
const relationshipManager = new LocationBasedRelationships();
const relatedEntities = relationshipManager.findRelatedEntitiesByLocation(allEntities, myEntity);
const locationGroups = relationshipManager.groupEntitiesByLocation(allEntities);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