Types and validation framework for Backstage's software catalog model with support for all entity kinds, relationships, and validation policies
93
Evaluation — 93%
↑ 1.05xAgent success when using this tile
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.
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>;
}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"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 failsValidates 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>;
}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'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"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;
}
}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');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()
);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();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-modeldocs
evals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10