Node.js utilities and types for building Backstage catalog modules, providing core APIs for catalog processors, entity providers, and processing workflows
—
Entity provider interface and related types for connecting external data sources to supply entities to the Backstage catalog. Entity providers enable integration with external systems to automatically discover and maintain entities in the catalog.
The main interface for implementing custom entity providers that supply entities from external data sources.
/**
* An entity provider is able to provide entities to the catalog.
* The provider is responsible for discovering, fetching, and maintaining entities from external sources.
*/
interface EntityProvider {
/**
* The name of the provider, which must be unique for all providers that are
* active in a catalog, and stable over time since emitted entities are
* related to the provider by this name.
*/
getProviderName(): string;
/**
* Called upon initialization by the catalog engine.
* This is where the provider should establish its connection and begin providing entities.
* @param connection - The connection to the catalog for applying mutations
*/
connect(connection: EntityProviderConnection): Promise<void>;
}Usage Examples:
import { EntityProvider, EntityProviderConnection } from "@backstage/plugin-catalog-node";
import { Entity } from "@backstage/catalog-model";
class GitHubOrgProvider implements EntityProvider {
private connection?: EntityProviderConnection;
private readonly org: string;
constructor(org: string) {
this.org = org;
}
getProviderName(): string {
return `github-org-${this.org}`;
}
async connect(connection: EntityProviderConnection): Promise<void> {
this.connection = connection;
// Initial full sync
await this.syncEntities();
// Set up periodic refresh
setInterval(() => this.syncEntities(), 5 * 60 * 1000); // Every 5 minutes
}
private async syncEntities(): Promise<void> {
if (!this.connection) return;
// Fetch repositories from GitHub API
const repos = await this.fetchRepositories();
const entities = repos.map(repo => ({
entity: {
apiVersion: "backstage.io/v1alpha1",
kind: "Component",
metadata: {
name: repo.name,
annotations: {
"github.com/project-slug": `${this.org}/${repo.name}`
}
},
spec: {
type: "service",
lifecycle: "production",
owner: this.org
}
} as Entity
}));
// Apply full mutation to replace all entities from this provider
await this.connection.applyMutation({
type: "full",
entities
});
}
private async fetchRepositories() {
// Implementation to fetch from GitHub API
return [];
}
}The connection interface between entity providers and the catalog, providing methods to apply mutations and trigger refreshes.
/**
* The connection between the catalog and the entity provider.
* Entity providers use this connection to add and remove entities from the catalog.
*/
interface EntityProviderConnection {
/**
* Applies either a full or a delta update to the catalog engine.
* @param mutation - The mutation to apply (full replacement or delta changes)
*/
applyMutation(mutation: EntityProviderMutation): Promise<void>;
/**
* Schedules a refresh on all of the entities that have a matching refresh key.
* @param options - Refresh options containing keys to refresh
*/
refresh(options: EntityProviderRefreshOptions): Promise<void>;
}Union type defining the types of mutations that can be applied to the catalog.
/**
* A 'full' mutation replaces all existing entities created by this entity provider with new ones.
* A 'delta' mutation can both add and remove entities provided by this provider.
*/
type EntityProviderMutation =
| {
type: 'full';
entities: DeferredEntity[];
}
| {
type: 'delta';
added: DeferredEntity[];
removed: (DeferredEntity | { entityRef: string; locationKey?: string })[];
};Usage Examples:
import { EntityProviderConnection, EntityProviderMutation } from "@backstage/plugin-catalog-node";
// Full mutation - replace all entities from this provider
const fullMutation: EntityProviderMutation = {
type: "full",
entities: [
{
entity: {
apiVersion: "backstage.io/v1alpha1",
kind: "Component",
metadata: { name: "service-a" },
spec: { type: "service", lifecycle: "production", owner: "team-alpha" }
}
},
{
entity: {
apiVersion: "backstage.io/v1alpha1",
kind: "Component",
metadata: { name: "service-b" },
spec: { type: "service", lifecycle: "production", owner: "team-beta" }
}
}
]
};
// Delta mutation - add and remove specific entities
const deltaMutation: EntityProviderMutation = {
type: "delta",
added: [
{
entity: {
apiVersion: "backstage.io/v1alpha1",
kind: "Component",
metadata: { name: "new-service" },
spec: { type: "service", lifecycle: "experimental", owner: "team-gamma" }
}
}
],
removed: [
{ entityRef: "component:default/old-service" },
{ entityRef: "component:default/deprecated-service", locationKey: "github-repo-123" }
]
};
// Apply mutations
async function updateCatalog(connection: EntityProviderConnection) {
await connection.applyMutation(fullMutation);
// Later, apply incremental changes
await connection.applyMutation(deltaMutation);
}Options for triggering entity refresh operations.
/**
* The options given to an entity refresh operation.
*/
type EntityProviderRefreshOptions = {
/**
* Array of refresh keys that should trigger entity refresh
*/
keys: string[];
};Usage Examples:
import { EntityProviderConnection, EntityProviderRefreshOptions } from "@backstage/plugin-catalog-node";
async function refreshEntities(connection: EntityProviderConnection) {
// Refresh entities with specific keys
const refreshOptions: EntityProviderRefreshOptions = {
keys: ["github-webhook-123", "scheduled-sync", "manual-trigger"]
};
await connection.refresh(refreshOptions);
}
// In a provider implementation
class WebhookEntityProvider implements EntityProvider {
private connection?: EntityProviderConnection;
getProviderName(): string {
return "webhook-provider";
}
async connect(connection: EntityProviderConnection): Promise<void> {
this.connection = connection;
// Set up webhook endpoint
this.setupWebhookHandler();
}
private setupWebhookHandler() {
// When webhook received, trigger refresh for affected entities
app.post('/webhook', async (req, res) => {
const { repository } = req.body;
if (this.connection) {
await this.connection.refresh({
keys: [`repo-${repository}`]
});
}
res.status(200).send('OK');
});
}
}Here's a complete example of an entity provider that integrates with an external API:
import {
EntityProvider,
EntityProviderConnection,
DeferredEntity
} from "@backstage/plugin-catalog-node";
import { Entity } from "@backstage/catalog-model";
interface ServiceInfo {
name: string;
team: string;
status: 'active' | 'deprecated';
}
class ServiceRegistryProvider implements EntityProvider {
private connection?: EntityProviderConnection;
private readonly apiUrl: string;
constructor(apiUrl: string) {
this.apiUrl = apiUrl;
}
getProviderName(): string {
return "service-registry";
}
async connect(connection: EntityProviderConnection): Promise<void> {
this.connection = connection;
// Initial sync
await this.fullSync();
// Schedule periodic full sync every hour
setInterval(() => this.fullSync(), 60 * 60 * 1000);
// Set up event-driven updates
this.setupEventHandlers();
}
private async fullSync(): Promise<void> {
if (!this.connection) return;
try {
const services = await this.fetchAllServices();
const entities: DeferredEntity[] = services
.filter(service => service.status === 'active') // Only include active services
.map(service => ({
entity: this.createEntityFromService(service),
locationKey: `service-${service.name}`
}));
await this.connection.applyMutation({
type: "full",
entities
});
console.log(`Synced ${entities.length} services from registry`);
} catch (error) {
console.error("Failed to sync services:", error);
}
}
private async handleServiceUpdate(serviceName: string): Promise<void> {
if (!this.connection) return;
try {
const service = await this.fetchService(serviceName);
if (service.status === 'active') {
// Add or update the service
await this.connection.applyMutation({
type: "delta",
added: [{
entity: this.createEntityFromService(service),
locationKey: `service-${service.name}`
}],
removed: []
});
} else {
// Remove deprecated service
await this.connection.applyMutation({
type: "delta",
added: [],
removed: [{
entityRef: `component:default/${service.name}`,
locationKey: `service-${service.name}`
}]
});
}
} catch (error) {
console.error(`Failed to update service ${serviceName}:`, error);
}
}
private createEntityFromService(service: ServiceInfo): Entity {
return {
apiVersion: "backstage.io/v1alpha1",
kind: "Component",
metadata: {
name: service.name,
annotations: {
"service-registry.company.com/id": service.name,
"service-registry.company.com/team": service.team
}
},
spec: {
type: "service",
lifecycle: service.status === 'active' ? 'production' : 'deprecated',
owner: service.team
}
};
}
private async fetchAllServices(): Promise<ServiceInfo[]> {
const response = await fetch(`${this.apiUrl}/services`);
return response.json();
}
private async fetchService(name: string): Promise<ServiceInfo> {
const response = await fetch(`${this.apiUrl}/services/${name}`);
return response.json();
}
private setupEventHandlers(): void {
// Set up webhooks or polling for service changes
// This would trigger handleServiceUpdate when services change
}
}Install with Tessl CLI
npx tessl i tessl/npm-backstage--plugin-catalog-node