Node.js utilities and types for building Backstage catalog modules, providing core APIs for catalog processors, entity providers, and processing workflows
—
Factory functions and types for creating standardized processing results with proper error handling and result typing. The processing results system provides a consistent way to emit various types of results during catalog processing operations.
A frozen object containing factory functions for creating standardized processing result types.
/**
* Factory functions for the standard processing result types.
* All functions return properly typed CatalogProcessorResult objects.
*/
const processingResult: {
/**
* Creates a not found error result
* @param atLocation - The location where the error occurred
* @param message - Error message describing what was not found
* @returns CatalogProcessorResult with NotFoundError
*/
notFoundError(atLocation: LocationSpec, message: string): CatalogProcessorResult;
/**
* Creates an input error result for invalid input data
* @param atLocation - The location where the error occurred
* @param message - Error message describing the input problem
* @returns CatalogProcessorResult with InputError
*/
inputError(atLocation: LocationSpec, message: string): CatalogProcessorResult;
/**
* Creates a general error result
* @param atLocation - The location where the error occurred
* @param message - Error message describing the problem
* @returns CatalogProcessorResult with generic Error
*/
generalError(atLocation: LocationSpec, message: string): CatalogProcessorResult;
/**
* Creates a location result for discovered locations
* @param newLocation - The new location that was discovered
* @returns CatalogProcessorResult containing the location
*/
location(newLocation: LocationSpec): CatalogProcessorResult;
/**
* Creates an entity result for discovered entities
* @param atLocation - The location where the entity was found
* @param newEntity - The entity that was discovered
* @returns CatalogProcessorResult containing the entity
*/
entity(atLocation: LocationSpec, newEntity: Entity): CatalogProcessorResult;
/**
* Creates a relation result for entity relations
* @param spec - The relation specification
* @returns CatalogProcessorResult containing the relation
*/
relation(spec: EntityRelationSpec): CatalogProcessorResult;
/**
* Creates a refresh result to trigger entity refresh
* @param key - The refresh key that should trigger a refresh
* @returns CatalogProcessorResult for refresh operation
*/
refresh(key: string): CatalogProcessorResult;
};Usage Examples:
import { processingResult, CatalogProcessor } from "@backstage/plugin-catalog-node";
import { Entity } from "@backstage/catalog-model";
import { LocationSpec } from "@backstage/plugin-catalog-common";
class ExampleProcessor implements CatalogProcessor {
getProcessorName(): string {
return "example-processor";
}
async readLocation(
location: LocationSpec,
optional: boolean,
emit: CatalogProcessorEmit,
parser: CatalogProcessorParser,
cache: CatalogProcessorCache
): Promise<boolean> {
try {
// Attempt to read the location
const data = await this.fetchLocationData(location);
if (!data) {
if (optional) {
// For optional locations, just skip
return true;
} else {
// For required locations, emit an error
emit(processingResult.notFoundError(
location,
`Location ${location.target} could not be found`
));
return true;
}
}
// Parse and validate the data
if (!this.isValidData(data)) {
emit(processingResult.inputError(
location,
"Invalid YAML structure in catalog file"
));
return true;
}
// Process the parsed entities
for await (const result of parser({ data, location })) {
if (result.type === 'entity') {
// Emit the entity
emit(processingResult.entity(location, result.entity));
// Check if entity references other locations
const refs = this.extractLocationReferences(result.entity);
for (const ref of refs) {
emit(processingResult.location(ref));
}
// Emit refresh key for this entity
emit(processingResult.refresh(`entity-${result.entity.metadata.name}`));
}
}
return true;
} catch (error) {
emit(processingResult.generalError(
location,
`Failed to process location: ${error.message}`
));
return true;
}
}
async preProcessEntity(
entity: Entity,
location: LocationSpec,
emit: CatalogProcessorEmit,
originLocation: LocationSpec,
cache: CatalogProcessorCache
): Promise<Entity> {
// Extract relations from entity
const relations = this.extractEntityRelations(entity);
// Emit each relation
for (const relation of relations) {
emit(processingResult.relation(relation));
}
return entity;
}
private async fetchLocationData(location: LocationSpec): Promise<Buffer | null> {
// Implementation to fetch data from location
return null;
}
private isValidData(data: Buffer): boolean {
// Implementation to validate data structure
return true;
}
private extractLocationReferences(entity: Entity): LocationSpec[] {
// Implementation to extract referenced locations
return [];
}
private extractEntityRelations(entity: Entity): EntityRelationSpec[] {
// Implementation to extract entity relations
return [];
}
}The union type encompassing all possible processor result types.
/**
* Union type of all possible catalog processor results
*/
type CatalogProcessorResult =
| CatalogProcessorLocationResult
| CatalogProcessorEntityResult
| CatalogProcessorRelationResult
| CatalogProcessorErrorResult
| CatalogProcessorRefreshKeysResult;Each result type serves a specific purpose in the catalog processing pipeline:
/**
* Result containing a discovered location that should be processed
*/
type CatalogProcessorLocationResult = {
type: 'location';
location: LocationSpec;
};
/**
* Result containing an entity discovered during processing
*/
type CatalogProcessorEntityResult = {
type: 'entity';
entity: Entity;
location: LocationSpec;
};
/**
* Result containing a relation between entities
*/
type CatalogProcessorRelationResult = {
type: 'relation';
relation: EntityRelationSpec;
};
/**
* Result containing an error that occurred during processing
*/
type CatalogProcessorErrorResult = {
type: 'error';
error: Error;
location: LocationSpec;
};
/**
* Result containing a refresh key to trigger entity refresh
*/
type CatalogProcessorRefreshKeysResult = {
type: 'refresh';
key: string;
};The processing results system supports different types of errors for various scenarios:
Use for missing resources that should exist:
import { processingResult } from "@backstage/plugin-catalog-node";
// When a required location cannot be found
emit(processingResult.notFoundError(
location,
`Catalog file not found at ${location.target}`
));
// When a referenced entity doesn't exist
emit(processingResult.notFoundError(
location,
`Referenced entity 'component:default/nonexistent' not found`
));Use for malformed or invalid data:
// When YAML is invalid
emit(processingResult.inputError(
location,
"Invalid YAML syntax: missing required 'metadata' field"
));
// When entity structure is wrong
emit(processingResult.inputError(
location,
"Entity must have 'apiVersion', 'kind', and 'metadata' fields"
));
// When validation fails
emit(processingResult.inputError(
location,
"Component entities must specify 'spec.type' and 'spec.owner'"
));Use for unexpected errors and system failures:
// When network requests fail
emit(processingResult.generalError(
location,
`Failed to fetch from ${url}: ${error.message}`
));
// When external API calls fail
emit(processingResult.generalError(
location,
`GitHub API error: ${response.status} ${response.statusText}`
));
// When parsing fails unexpectedly
emit(processingResult.generalError(
location,
`Unexpected parsing error: ${error.message}`
));class ConditionalProcessor implements CatalogProcessor {
getProcessorName(): string {
return "conditional-processor";
}
async preProcessEntity(
entity: Entity,
location: LocationSpec,
emit: CatalogProcessorEmit,
originLocation: LocationSpec,
cache: CatalogProcessorCache
): Promise<Entity> {
// Only process certain entity types
if (entity.kind !== 'Component') {
return entity;
}
// Check cache to avoid duplicate processing
const lastProcessed = await cache.get<number>('last-processed');
const now = Date.now();
if (lastProcessed && (now - lastProcessed) < 60000) {
// Skip if processed recently
return entity;
}
// Emit refresh key for this component
emit(processingResult.refresh(`component-${entity.metadata.name}`));
// Update cache
await cache.set('last-processed', now);
return entity;
}
}class BatchProcessor implements CatalogProcessor {
getProcessorName(): string {
return "batch-processor";
}
async readLocation(
location: LocationSpec,
optional: boolean,
emit: CatalogProcessorEmit,
parser: CatalogProcessorParser,
cache: CatalogProcessorCache
): Promise<boolean> {
if (!location.target.includes('batch-catalog')) {
return false;
}
try {
const batchData = await this.fetchBatchData(location);
// Process each item in the batch
for (const [index, item] of batchData.entries()) {
try {
const entity = this.transformToEntity(item);
emit(processingResult.entity(location, entity));
// Emit relations if they exist
if (item.relations) {
for (const relation of item.relations) {
emit(processingResult.relation(relation));
}
}
} catch (itemError) {
emit(processingResult.inputError(
location,
`Error processing batch item ${index}: ${itemError.message}`
));
}
}
return true;
} catch (error) {
emit(processingResult.generalError(
location,
`Failed to process batch location: ${error.message}`
));
return true;
}
}
private async fetchBatchData(location: LocationSpec): Promise<any[]> {
// Implementation to fetch batch data
return [];
}
private transformToEntity(item: any): Entity {
// Implementation to transform batch item to entity
return {} as Entity;
}
}Install with Tessl CLI
npx tessl i tessl/npm-backstage--plugin-catalog-node