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

location-management.mddocs/

Location Management

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.

Capabilities

Location Annotations

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"
  }
};

Location Reference Parsing

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" }

Location Reference Stringification

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 === originalRef

Entity Source Location Extraction

Extract 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");
}

Location Management Patterns

Location-Based Entity Processing

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;
  }
}

Location Validation and Normalization

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/");

Location-Based Entity Discovery

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);
}

Location-Based Entity Relationships

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-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