CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-backstage--plugin-catalog-node

Node.js utilities and types for building Backstage catalog modules, providing core APIs for catalog processors, entity providers, and processing workflows

Pending
Overview
Eval results
Files

location-conversion.mddocs/

Location Conversion

Utility functions for converting location specifications into standardized formats and Location entities. These functions provide standardized ways to generate machine-readable names and create Location entities from location specifications.

Capabilities

locationSpecToMetadataName Function

Generates a standard machine-readable name for a location specification using SHA-1 hashing.

/**
 * A standard way of producing a machine generated name for a location.
 * Creates a consistent, deterministic name based on location type and target.
 * @param location - The location specification to generate a name for
 * @returns A generated name in the format "generated-{hash}"
 */
function locationSpecToMetadataName(location: LocationSpec): string;

Usage Examples:

import { locationSpecToMetadataName } from "@backstage/plugin-catalog-node";
import { LocationSpec } from "@backstage/plugin-catalog-common";

// Generate name for a URL location
const urlLocation: LocationSpec = {
  type: "url",
  target: "https://github.com/backstage/backstage/blob/master/catalog-info.yaml"
};

const urlName = locationSpecToMetadataName(urlLocation);
console.log(urlName); // "generated-a1b2c3d4e5f6..."

// Generate name for a file location
const fileLocation: LocationSpec = {
  type: "file",
  target: "/path/to/catalog-info.yaml"
};

const fileName = locationSpecToMetadataName(fileLocation);
console.log(fileName); // "generated-f6e5d4c3b2a1..."

// Same input always produces same output
const duplicate = locationSpecToMetadataName(urlLocation);
console.log(urlName === duplicate); // true

The function creates a SHA-1 hash of the combination ${location.type}:${location.target} and returns it with the prefix "generated-". This ensures:

  • Deterministic: Same location always produces the same name
  • Unique: Different locations produce different names
  • Machine-readable: Generated names follow a consistent format
  • Collision-resistant: SHA-1 provides good distribution for typical use cases

locationSpecToLocationEntity Function

Converts a location specification into a standard Location entity with proper annotations and metadata.

/**
 * A standard way of producing a machine generated Location kind entity for a location.
 * Creates a complete LocationEntityV1alpha1 with proper annotations and metadata.
 * @param opts - Options containing the location and optional parent entity
 * @param opts.location - The location specification to convert
 * @param opts.parentEntity - Optional parent entity for nested locations
 * @returns A complete Location entity
 */
function locationSpecToLocationEntity(opts: {
  location: LocationSpec;
  parentEntity?: Entity;
}): LocationEntityV1alpha1;

Usage Examples:

import { locationSpecToLocationEntity } from "@backstage/plugin-catalog-node";
import { LocationSpec } from "@backstage/plugin-catalog-common";
import { Entity } from "@backstage/catalog-model";

// Create Location entity for a standalone location
const location: LocationSpec = {
  type: "url",
  target: "https://github.com/my-org/my-repo/blob/main/catalog-info.yaml",
  presence: "required"
};

const locationEntity = locationSpecToLocationEntity({ location });

console.log(locationEntity);
// {
//   apiVersion: "backstage.io/v1alpha1",
//   kind: "Location",
//   metadata: {
//     name: "generated-abc123...",
//     annotations: {
//       "backstage.io/managed-by-location": "url:https://github.com/my-org/my-repo/blob/main/catalog-info.yaml",
//       "backstage.io/managed-by-origin-location": "url:https://github.com/my-org/my-repo/blob/main/catalog-info.yaml"
//     }
//   },
//   spec: {
//     type: "url",
//     target: "https://github.com/my-org/my-repo/blob/main/catalog-info.yaml",
//     presence: "required"
//   }
// }

// Create Location entity with parent context
const parentEntity: Entity = {
  apiVersion: "backstage.io/v1alpha1",
  kind: "Component",
  metadata: {
    name: "parent-component",
    annotations: {
      "backstage.io/managed-by-location": "url:https://github.com/parent/repo/catalog-info.yaml",
      "backstage.io/managed-by-origin-location": "url:https://github.com/parent/repo/catalog-info.yaml"
    }
  },
  spec: {
    type: "service",
    lifecycle: "production",
    owner: "team-a"
  }
};

const childLocation: LocationSpec = {
  type: "url", 
  target: "https://github.com/child/repo/catalog-info.yaml"
};

const childLocationEntity = locationSpecToLocationEntity({
  location: childLocation,
  parentEntity
});

console.log(childLocationEntity.metadata.annotations);
// {
//   "backstage.io/managed-by-location": "url:https://github.com/parent/repo/catalog-info.yaml",
//   "backstage.io/managed-by-origin-location": "url:https://github.com/parent/repo/catalog-info.yaml"
// }

Location Entity Structure

The generated Location entities follow the standard Backstage Location schema:

/**
 * Location entity structure created by locationSpecToLocationEntity
 */
interface LocationEntityV1alpha1 {
  apiVersion: "backstage.io/v1alpha1";
  kind: "Location";
  metadata: {
    name: string; // Generated using locationSpecToMetadataName
    annotations: {
      "backstage.io/managed-by-location": string;
      "backstage.io/managed-by-origin-location": string;
    };
  };
  spec: {
    type: string;
    target: string;
    presence?: "required" | "optional";
  };
}

Annotation Behavior

The function handles annotations differently based on whether a parent entity is provided:

Without Parent Entity:

  • managed-by-location: Set to the location's stringified reference
  • managed-by-origin-location: Set to the same value as managed-by-location

With Parent Entity:

  • managed-by-location: Copied from parent entity's managed-by-location annotation
  • managed-by-origin-location: Copied from parent entity's managed-by-origin-location annotation

This creates a proper chain of custody for nested location discoveries.

Advanced Usage Patterns

Processor Integration

import { 
  locationSpecToLocationEntity,
  locationSpecToMetadataName,
  CatalogProcessor,
  processingResult
} from "@backstage/plugin-catalog-node";

class LocationDiscoveryProcessor implements CatalogProcessor {
  getProcessorName(): string {
    return "location-discovery";
  }

  async preProcessEntity(
    entity: Entity,
    location: LocationSpec,
    emit: CatalogProcessorEmit,
    originLocation: LocationSpec,
    cache: CatalogProcessorCache
  ): Promise<Entity> {
    // Discover referenced locations in entity
    const referencedLocations = this.extractLocationReferences(entity);
    
    for (const refLocation of referencedLocations) {
      // Create Location entity for each discovered location
      const locationEntity = locationSpecToLocationEntity({
        location: refLocation,
        parentEntity: entity
      });
      
      // Emit the Location entity
      emit(processingResult.entity(location, locationEntity));
      
      // Also emit the location for processing
      emit(processingResult.location(refLocation));
      
      // Store in cache for deduplication
      const locName = locationSpecToMetadataName(refLocation);
      await cache.set(`discovered-${locName}`, true);
    }

    return entity;
  }

  private extractLocationReferences(entity: Entity): LocationSpec[] {
    const locations: LocationSpec[] = [];
    
    // Check for catalog-info references in annotations
    const catalogInfoRef = entity.metadata.annotations?.['backstage.io/catalog-info-url'];
    if (catalogInfoRef) {
      locations.push({
        type: "url",
        target: catalogInfoRef
      });
    }
    
    // Check for documentation references
    const docsRef = entity.metadata.annotations?.['backstage.io/docs-url'];
    if (docsRef) {
      locations.push({
        type: "url",
        target: docsRef,
        presence: "optional"
      });
    }
    
    return locations;
  }
}

Batch Location Creation

import { locationSpecToLocationEntity, locationSpecToMetadataName } from "@backstage/plugin-catalog-node";

interface LocationDiscoveryResult {
  locations: LocationSpec[];
  parentEntity?: Entity;
}

class LocationBatchProcessor {
  async processDiscoveredLocations(
    result: LocationDiscoveryResult
  ): Promise<Entity[]> {
    const locationEntities: Entity[] = [];
    
    for (const location of result.locations) {
      // Check if we should create this location
      if (await this.shouldCreateLocation(location)) {
        const locationEntity = locationSpecToLocationEntity({
          location,
          parentEntity: result.parentEntity
        });
        
        locationEntities.push(locationEntity);
        
        // Log creation
        const name = locationSpecToMetadataName(location);
        console.log(`Created location entity: ${name} for ${location.target}`);
      }
    }
    
    return locationEntities;
  }

  private async shouldCreateLocation(location: LocationSpec): Promise<boolean> {
    // Check if location already exists in catalog
    const name = locationSpecToMetadataName(location);
    const exists = await this.checkLocationExists(name);
    
    return !exists;
  }

  private async checkLocationExists(name: string): Promise<boolean> {
    // Implementation to check if location entity already exists
    return false;
  }
}

Location Validation

import { locationSpecToLocationEntity } from "@backstage/plugin-catalog-node";

class LocationValidator {
  validateLocationEntity(location: LocationSpec, parentEntity?: Entity): string[] {
    const errors: string[] = [];
    
    try {
      const locationEntity = locationSpecToLocationEntity({
        location,
        parentEntity
      });
      
      // Validate the generated entity
      if (!locationEntity.metadata.name) {
        errors.push("Generated location entity missing name");
      }
      
      if (!locationEntity.spec.target) {
        errors.push("Location entity missing target");
      }
      
      if (!locationEntity.spec.type) {
        errors.push("Location entity missing type");
      }
      
      // Validate annotations if parent entity provided
      if (parentEntity) {
        const requiredAnnotations = [
          "backstage.io/managed-by-location",
          "backstage.io/managed-by-origin-location"
        ];
        
        for (const annotation of requiredAnnotations) {
          if (!parentEntity.metadata.annotations?.[annotation]) {
            errors.push(`Parent entity missing required annotation: ${annotation}`);
          }
        }
      }
      
    } catch (error) {
      errors.push(`Failed to create location entity: ${error.message}`);
    }
    
    return errors;
  }
}

Install with Tessl CLI

npx tessl i tessl/npm-backstage--plugin-catalog-node

docs

alpha-apis.md

catalog-processing.md

entity-providers.md

index.md

location-conversion.md

processing-results.md

tile.json