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

processing-results.mddocs/

Processing Results

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.

Capabilities

processingResult Factory Object

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

CatalogProcessorResult Union Type

The union type encompassing all possible processor result types.

/**
 * Union type of all possible catalog processor results
 */
type CatalogProcessorResult =
  | CatalogProcessorLocationResult
  | CatalogProcessorEntityResult
  | CatalogProcessorRelationResult
  | CatalogProcessorErrorResult
  | CatalogProcessorRefreshKeysResult;

Individual Result Types

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

Error Handling Patterns

The processing results system supports different types of errors for various scenarios:

Not Found Errors

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

Input Errors

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

General Errors

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

Advanced Usage Patterns

Conditional Result Emission

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

Batch Result Processing

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

docs

alpha-apis.md

catalog-processing.md

entity-providers.md

index.md

location-conversion.md

processing-results.md

tile.json