CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-backstage--catalog-model

Types and validation framework for Backstage's software catalog model with support for all entity kinds, relationships, and validation policies

93

1.05x

Evaluation93%

1.05x

Agent success when using this tile

Overview
Eval results
Files

entity-policies.mddocs/

Entity Policies

Rule-based entity processing system for validation and mutation during entity ingestion and processing. Entity policies provide a pluggable architecture for implementing custom business rules, validation logic, and automatic entity transformations.

Capabilities

Entity Policy Interface

The base interface that all entity policies must implement to participate in entity processing.

/**
 * Interface for entity policies that can validate and transform entities
 */
interface EntityPolicy {
  /**
   * Enforce the policy on an entity
   * @param entity - Entity to process
   * @returns Promise resolving to transformed entity, or undefined if policy fails
   */
  enforce(entity: Entity): Promise<Entity | undefined>;
}

Default Namespace Policy

Automatically sets the default namespace for entities that don't specify one explicitly.

/**
 * Policy that sets a default namespace if none is specified
 */
class DefaultNamespaceEntityPolicy implements EntityPolicy {
  /**
   * Create a default namespace policy
   * @param namespace - Namespace to use as default (defaults to 'default')
   */
  constructor(namespace?: string);
  
  /**
   * Enforce the default namespace policy
   * @param entity - Entity to process
   * @returns Entity with namespace set to default if not specified
   */
  enforce(entity: Entity): Promise<Entity>;
}

Usage Example:

import { DefaultNamespaceEntityPolicy, Entity } from "@backstage/catalog-model";

const policy = new DefaultNamespaceEntityPolicy("production");

const entity: Entity = {
  apiVersion: "backstage.io/v1alpha1",
  kind: "Component",
  metadata: {
    name: "user-service"
    // No namespace specified
  }
};

const processedEntity = await policy.enforce(entity);
// Result: entity.metadata.namespace will be "production"

Field Format Policy

Validates entity fields using configurable validator functions to ensure proper formatting and constraints.

/**
 * Policy that validates entity field formats using validator functions
 */
class FieldFormatEntityPolicy implements EntityPolicy {
  /**
   * Create a field format validation policy
   * @param validators - Optional validator functions (uses defaults if not provided)
   */
  constructor(validators?: Validators);
  
  /**
   * Enforce field format validation
   * @param entity - Entity to validate
   * @returns Entity if all fields are valid, undefined if validation fails
   */
  enforce(entity: Entity): Promise<Entity>;
}

Usage Example:

import { 
  FieldFormatEntityPolicy, 
  makeValidator,
  Entity 
} from "@backstage/catalog-model";

// Use custom validators
const customValidators = makeValidator({
  entityName: (value) => typeof value === 'string' && /^[a-z-]+$/.test(value)
});

const policy = new FieldFormatEntityPolicy(customValidators);

const entity: Entity = {
  apiVersion: "backstage.io/v1alpha1",
  kind: "Component",
  metadata: {
    name: "user-service", // Valid: lowercase with hyphens
    namespace: "default"
  }
};

const result = await policy.enforce(entity);
// Returns entity if valid, undefined if validation fails

Schema Validation Policy

Validates entities against their base JSON schema to ensure structural correctness.

/**
 * Policy that validates entities against their base schema
 */
class SchemaValidEntityPolicy implements EntityPolicy {
  /**
   * Create a schema validation policy
   */
  constructor();
  
  /**
   * Enforce schema validation
   * @param entity - Entity to validate against schema
   * @returns Entity if schema validation passes, undefined if it fails
   */
  enforce(entity: Entity): Promise<Entity>;
}

No Foreign Root Fields Policy

Ensures entities don't contain unknown root-level fields beyond the standard entity structure.

/**
 * Policy that removes or validates against unknown root fields
 */
class NoForeignRootFieldsEntityPolicy implements EntityPolicy {
  /**
   * Create a policy that validates root fields
   * @param knownFields - Array of allowed root field names
   */
  constructor(knownFields?: string[]);
  
  /**
   * Enforce no foreign root fields policy
   * @param entity - Entity to validate
   * @returns Entity with only known root fields
   */
  enforce(entity: Entity): Promise<Entity>;
}

Usage Example:

import { NoForeignRootFieldsEntityPolicy } from "@backstage/catalog-model";

const policy = new NoForeignRootFieldsEntityPolicy([
  'apiVersion', 'kind', 'metadata', 'spec', 'status' // Allow status field
]);

const entityWithExtraFields = {
  apiVersion: "backstage.io/v1alpha1",
  kind: "Component",
  metadata: { name: "test" },
  spec: { type: "service" },
  customField: "should be removed", // This will be filtered out
  status: { phase: "active" }        // This will be kept
};

const cleanEntity = await policy.enforce(entityWithExtraFields);
// Result: cleanEntity will not contain 'customField'

Group Default Parent Policy

Automatically sets a default parent for group entities that don't specify one, enabling organizational hierarchy management.

/**
 * Policy that sets a default parent for group entities
 */
class GroupDefaultParentEntityPolicy implements EntityPolicy {
  /**
   * Create a group default parent policy
   * @param parentEntityRef - Reference to the default parent group
   */
  constructor(parentEntityRef: string);
  
  /**
   * Enforce default parent assignment for groups
   * @param entity - Entity to process (only affects Group entities)
   * @returns Entity with default parent set if it's a Group without a parent
   */
  enforce(entity: Entity): Promise<Entity>;
}

Usage Example:

import { 
  GroupDefaultParentEntityPolicy, 
  GroupEntity 
} from "@backstage/catalog-model";

const policy = new GroupDefaultParentEntityPolicy("group:default/engineering");

const group: GroupEntity = {
  apiVersion: "backstage.io/v1alpha1",
  kind: "Group",
  metadata: {
    name: "frontend-team"
  },
  spec: {
    type: "team",
    children: []
    // No parent specified
  }
};

const processedGroup = await policy.enforce(group);
// Result: processedGroup.spec.parent will be "group:default/engineering"

Policy Combinators

Utility functions for combining multiple policies into complex validation and transformation pipelines.

/**
 * Utility object for combining entity policies
 */
const EntityPolicies: {
  /**
   * Create a policy that requires all sub-policies to succeed
   * @param policies - Array of policies that must all pass
   * @returns Combined policy that applies all policies in sequence
   */
  allOf(policies: EntityPolicy[]): EntityPolicy;
  
  /**
   * Create a policy that requires at least one sub-policy to succeed
   * @param policies - Array of policies where at least one must pass
   * @returns Combined policy that tries policies until one succeeds
   */
  oneOf(policies: EntityPolicy[]): EntityPolicy;
};

Usage Examples:

import { 
  EntityPolicies,
  DefaultNamespaceEntityPolicy,
  FieldFormatEntityPolicy,
  SchemaValidEntityPolicy
} from "@backstage/catalog-model";

// Create a comprehensive validation pipeline
const validationPipeline = EntityPolicies.allOf([
  new SchemaValidEntityPolicy(),
  new DefaultNamespaceEntityPolicy("default"),
  new FieldFormatEntityPolicy(),
  new NoForeignRootFieldsEntityPolicy()
]);

// Alternative validation (try different approaches)
const flexibleValidation = EntityPolicies.oneOf([
  EntityPolicies.allOf([
    new SchemaValidEntityPolicy(),
    new FieldFormatEntityPolicy()
  ]),
  // Fallback: just basic schema validation
  new SchemaValidEntityPolicy()
]);

// Usage
async function processEntity(entity: Entity): Promise<Entity | null> {
  try {
    const processedEntity = await validationPipeline.enforce(entity);
    return processedEntity || null;
  } catch (error) {
    console.error("Entity processing failed:", error);
    return null;
  }
}

Policy Implementation Patterns

Custom Entity Policy

import { Entity, EntityPolicy, ComponentEntity, isComponentEntity } from "@backstage/catalog-model";

class ComponentOwnershipPolicy implements EntityPolicy {
  private readonly requiredOwnerDomain: string;
  
  constructor(requiredOwnerDomain: string) {
    this.requiredOwnerDomain = requiredOwnerDomain;
  }
  
  async enforce(entity: Entity): Promise<Entity | undefined> {
    // Only apply to Component entities
    if (!isComponentEntity(entity)) {
      return entity;
    }
    
    // Validate owner format
    const owner = entity.spec.owner;
    if (!owner.includes(this.requiredOwnerDomain)) {
      throw new Error(
        `Component owner must include domain: ${this.requiredOwnerDomain}`
      );
    }
    
    // Add ownership annotation
    return {
      ...entity,
      metadata: {
        ...entity.metadata,
        annotations: {
          ...entity.metadata.annotations,
          'company.com/owner-domain': this.requiredOwnerDomain
        }
      }
    };
  }
}

// Usage
const ownershipPolicy = new ComponentOwnershipPolicy('@mycompany.com');

Conditional Policy Application

import { Entity, EntityPolicy } from "@backstage/catalog-model";

class ConditionalPolicy implements EntityPolicy {
  constructor(
    private condition: (entity: Entity) => boolean,
    private policy: EntityPolicy
  ) {}
  
  async enforce(entity: Entity): Promise<Entity | undefined> {
    if (this.condition(entity)) {
      return this.policy.enforce(entity);
    }
    return entity;
  }
}

// Apply different policies based on entity kind
const conditionalValidation = new ConditionalPolicy(
  (entity) => entity.kind === 'Component',
  new FieldFormatEntityPolicy()
);

Policy Chain Builder

import { Entity, EntityPolicy } from "@backstage/catalog-model";

class PolicyChainBuilder {
  private policies: EntityPolicy[] = [];
  
  add(policy: EntityPolicy): this {
    this.policies.push(policy);
    return this;
  }
  
  addIf(condition: boolean, policy: EntityPolicy): this {
    if (condition) {
      this.policies.push(policy);
    }
    return this;
  }
  
  build(): EntityPolicy {
    return EntityPolicies.allOf(this.policies);
  }
}

// Usage
const isDevelopment = process.env.NODE_ENV === 'development';

const policy = new PolicyChainBuilder()
  .add(new SchemaValidEntityPolicy())
  .add(new DefaultNamespaceEntityPolicy())
  .addIf(isDevelopment, new NoForeignRootFieldsEntityPolicy())
  .build();

Async Policy Processing

import { Entity, EntityPolicy } from "@backstage/catalog-model";

async function processEntitiesWithPolicies(
  entities: Entity[], 
  policies: EntityPolicy[]
): Promise<Array<{entity?: Entity, error?: string}>> {
  const combinedPolicy = EntityPolicies.allOf(policies);
  
  return Promise.all(
    entities.map(async (entity, index) => {
      try {
        const processedEntity = await combinedPolicy.enforce(entity);
        return processedEntity ? { entity: processedEntity } : { error: "Policy rejected entity" };
      } catch (error) {
        return { 
          error: `Entity ${index}: ${error instanceof Error ? error.message : 'Unknown error'}` 
        };
      }
    })
  );
}

Install with Tessl CLI

npx tessl i tessl/npm-backstage--catalog-model

docs

core-entities.md

entity-kinds.md

entity-policies.md

entity-references.md

entity-relations.md

index.md

location-management.md

validation.md

tile.json