Node.js utilities and types for building Backstage catalog modules, providing core APIs for catalog processors, entity providers, and processing workflows
—
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.
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); // trueThe function creates a SHA-1 hash of the combination ${location.type}:${location.target} and returns it with the prefix "generated-". This ensures:
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"
// }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";
};
}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 referencemanaged-by-origin-location: Set to the same value as managed-by-locationWith Parent Entity:
managed-by-location: Copied from parent entity's managed-by-location annotationmanaged-by-origin-location: Copied from parent entity's managed-by-origin-location annotationThis creates a proper chain of custody for nested location discoveries.
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;
}
}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;
}
}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