Repository: Parsifal-M/backstage-catalog-gen
Pull request #8: Add generator function type to support all Entity kinds
Author: login: kurtaking
Created: 2025-03-17T15:36:23Z | Merged: 2025-03-18T15:10:10Z

Resolves https://github.com/Parsifal-M/backstage-catalog-gen/issues/7

* renamed references of "entity type" to "entity kind"
* added a registry of entity generators, one for each entity kind
* leverage a  generic `createBaseEntity` function for shared fields (`apiVersion`, `description`, etc..)
* update UI to generate a list of `<MenuItem />` based on the generator registry keys
* update UI to hide owner `<TextField />` when a `User` or `Group` entity is selected to be generated

## Generator Interface

```ts
export interface EntityGeneratorParams<T> {
  name: string;
  owner?: string;
  annotations?: Record<string, string>;
  spec?: Partial<T>;
}

export type EntityGenerator<T> = (params: EntityGeneratorParams<T>) => Entity;

export function createBaseEntity<T>(
  kind: EntityKind,
  params: EntityGeneratorParams<T>,
  specGenerator: (params: EntityGeneratorParams<T>) => Record<string, unknown>
): Entity
```

## Example

```ts
export const generateComponentEntity = (params: EntityGeneratorParams<ComponentEntitySpec>): Entity => {
  const componentTypes = ["service", "website", "library"];
  const lifecycleStates = ["experimental", "production", "deprecated"];

  const defaultSpec = {
    type: componentTypes[Math.floor(Math.random() * componentTypes.length)],
    lifecycle: lifecycleStates[0]
  };

  return createBaseEntity<ComponentEntitySpec>(
    "Component",
    params,
    (p: EntityGeneratorParams<ComponentEntitySpec>) => ({
      ...defaultSpec,
      ...p.spec
    })
  );
};
```

## Registry

A lookup map of entity generators is created

```ts
/**
 * Registry of entity generators
 */
export const generators: {
  [K in keyof EntitySpecMap]: EntityGenerator<EntitySpecMap[K]>
} = {
  API: generateApiEntity,
  Component: generateComponentEntity,
  Domain: generateDomainEntity,
  Group: generateGroupEntity,
  User: generateUserEntity,
  Resource: generateResourceEntity,
  System: generateSystemEntity,
  Location: generateLocationEntity,
  Template: generateTemplateEntity
};

/**
 * Generates an entity of a given kind
 */
export const generateEntity = <K extends keyof EntitySpecMap>(
  kind: K,
  params: EntityGeneratorParams<EntitySpecMap[K]>
): Entity => {
  const generator = generators[kind];
  if (!generator) {
    throw new Error(`Unknown entity kind: ${kind}. Available kinds: ${Object.keys(generators).join(', ')}`);
  }

  return generator(params);
};
```

## Updated `generateEntities`

```ts
for (let i = 0; i < amount; i++) {
    const entityName = `example-${faker.hacker.abbreviation().toLowerCase()}-${i + 1}`;

   // now delegates entity creation to the generator
    const entity = generateEntity(entityKind, {
      name: entityName,
      owner: owner || undefined,
      annotations,
      spec: {}  // todo: pass any custom spec options if needed
    });

    entities.push(entity);
  }
```


Diff:
diff --git a/README.md b/README.md
index d45209f..e1d216d 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@ Sometimes I want to load up the catalog with a bunch of entities and it was a bi
 
 ## What Next?
 
-I don't really know, it serves its current purpose well enough, but I am open to any suggestions or improvements. It would be nice to maybe allow for adding custom annotations and tags? Maybe later!
+I don't really know, it serves its current purpose well enough, but I am open to any suggestions or improvements. It would be nice to maybe allow for adding custom metadata in addition to annotations? Maybe later!
 
 ## Contributing
 
diff --git a/src/App.tsx b/src/App.tsx
index 301bd73..6a7fa0f 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -2,7 +2,7 @@ import { useState } from "react";
 import { EntityForm } from "./components/EntityForm";
 import { EntityOutput } from "./components/EntityOutput";
 import { generateEntities } from "./utils/generateEntities";
-import { EntityType } from "./types";
+import { EntityKind } from "./types";
 import {
   ThemeProvider,
   createTheme,
@@ -39,7 +39,7 @@ function App() {
   const [yamlContent, setYamlContent] = useState<string>("");
 
   const handleGenerate = (
-    entityType: EntityType,
+    entityType: EntityKind,
     amount: number,
     owner?: string | null,
     annotations: Record<string, string> = {}
diff --git a/src/components/EntityForm.tsx b/src/components/EntityForm.tsx
index 07f68ec..e2a6740 100644
--- a/src/components/EntityForm.tsx
+++ b/src/components/EntityForm.tsx
@@ -1,5 +1,4 @@
-import { useState } from "react";
-import { EntityType } from "../types";
+import { useEffect, useState } from "react";
 import {
   TextField,
   Select,
@@ -13,10 +12,11 @@ import {
   Stack,
 } from "@mui/material";
 import { parseAnnotations } from "../utils/parseAnnotations";
-
+import { EntityKind } from "../types";
+import { generators } from "../generators/registry";
 interface EntityFormProps {
   onGenerate: (
-    type: EntityType,
+    kind: EntityKind,
     amount: number,
     owner?: string | null,
     annotations?: Record<string, string>
@@ -24,11 +24,12 @@ interface EntityFormProps {
 }
 
 export const EntityForm = ({ onGenerate }: EntityFormProps) => {
-  const [entityType, setEntityType] = useState<EntityType>("Component");
+  const [entityKind, setEntityKind] = useState<EntityKind>("Component");
   const [amount, setAmount] = useState<number>(1);
   const [owner, setOwner] = useState<string | null>(null);
   const [error, setError] = useState<string | null>(null);
   const [annotationsText, setAnnotationsText] = useState<string>("");
+  const [showOwnerField, setShowOwnerField] = useState<boolean>(true);
 
   const handleSubmit = (e: React.FormEvent) => {
     e.preventDefault();
@@ -40,9 +41,13 @@ export const EntityForm = ({ onGenerate }: EntityFormProps) => {
 
     setError(null);
     const parsedAnnotations = parseAnnotations(annotationsText);
-    onGenerate(entityType, amount, owner, parsedAnnotations);
+    onGenerate(entityKind, amount, owner, parsedAnnotations);
   };
 
+  useEffect(() => {
+    setShowOwnerField(!["User", "Group"].includes(entityKind));
+  }, [entityKind]);
+
   return (
     <Paper
       elevation={3}
@@ -73,19 +78,19 @@ export const EntityForm = ({ onGenerate }: EntityFormProps) => {
       <Box component="form" onSubmit={handleSubmit} noValidate>
         <Stack spacing={3}>
           <FormControl fullWidth variant="outlined">
-            <InputLabel id="entity-type-label">Entity Type</InputLabel>
+            <InputLabel id="entity-kind-label">Entity Kind</InputLabel>
             <Select
-              labelId="entity-type-label"
-              id="entityType"
-              value={entityType}
-              onChange={(e) => setEntityType(e.target.value as EntityType)}
-              label="Entity Type"
+              labelId="entity-kind-label"
+              id="entityKind"
+              value={entityKind}
+              onChange={(e) => setEntityKind(e.target.value as EntityKind)}
+              label="Entity Kind"
             >
-              <MenuItem value="Component">Component</MenuItem>
-              <MenuItem value="API">API</MenuItem>
-              <MenuItem value="System">System</MenuItem>
-              <MenuItem value="Domain">Domain</MenuItem>
-              <MenuItem value="Resource">Resource</MenuItem>
+              {Object.keys(generators).map((kind) => (
+                <MenuItem key={kind} value={kind}>
+                  {kind}
+                </MenuItem>
+              ))}
             </Select>
           </FormControl>
 
@@ -104,7 +109,7 @@ export const EntityForm = ({ onGenerate }: EntityFormProps) => {
             helperText={error ?? "Number of entities to generate (1-50)"}
           />
 
-          <TextField
+          {showOwnerField && (<TextField
             fullWidth
             id="owner"
             label="Owner (optional)"
@@ -115,7 +120,7 @@ export const EntityForm = ({ onGenerate }: EntityFormProps) => {
               setOwner(e.target.value)
             }
             helperText="Owner of the entities"
-          />
+          />)}
 
           <TextField
             fullWidth
diff --git a/src/generators/createBaseEntity.ts b/src/generators/createBaseEntity.ts
new file mode 100644
index 0000000..c6c67df
--- /dev/null
+++ b/src/generators/createBaseEntity.ts
@@ -0,0 +1,44 @@
+import { faker } from "@faker-js/faker";
+import { Entity, EntityKind } from "../types";
+import { EntityGeneratorParams } from "./types";
+
+export function createBaseEntity<T>(
+  kind: EntityKind,
+  params: EntityGeneratorParams<T>,
+  specGenerator: (params: EntityGeneratorParams<T>) => Record<string, unknown>
+): Entity {
+  const { name, annotations, owner } = params;
+
+  return {
+    apiVersion: kind === "Template" ? "backstage.io/v1beta2" : "backstage.io/v1alpha1",
+    kind,
+    metadata: {
+      name,
+      description: faker.commerce.productDescription(),
+      ...(annotations && Object.keys(annotations).length > 0 ? { annotations } : null)
+    },
+    spec: {
+      owner: owner ?? getDefaultOwnerForKind(kind),
+      ...specGenerator(params)
+    }
+  };
+}
+
+/**
+ * Helper to provide default owners by entity kind when no owner is provided in the params
+ */
+function getDefaultOwnerForKind(kind: string): string {
+  const defaultOwners: Record<string, string> = {
+    'Component': 'team-a',
+    'API': 'team-b',
+    'System': 'team-b',
+    'Template': 'team-platform',
+    'Domain': 'team-platform',
+    'Group': 'team-platform',
+    'Location': 'team-platform',
+    'Resource': 'team-platform',
+    'User': 'team-platform',
+  };
+
+  return defaultOwners[kind] || 'team-a';
+}
diff --git a/src/generators/kinds/api.ts b/src/generators/kinds/api.ts
new file mode 100644
index 0000000..8a74e7c
--- /dev/null
+++ b/src/generators/kinds/api.ts
@@ -0,0 +1,35 @@
+import { Entity } from "../../types";
+import { EntityGeneratorParams, BaseEntitySpec } from "../types";
+import { createBaseEntity } from "../createBaseEntity";
+
+export interface ApiEntitySpec extends BaseEntitySpec {
+  type: string;
+  lifecycle: string;
+  owner: string;
+  system?: string;
+  definition: string;
+}
+
+export const generateApiEntity = (params: EntityGeneratorParams<ApiEntitySpec>): Entity => {
+  const { name, owner } = params;
+  const defaultSpec = {
+    type: "openapi",
+    lifecycle: "production",
+    owner: owner ?? "team-b",
+    definition: `openapi: "3.0.0"
+info:
+  version: 1.0.0
+  title: ${name}
+  license:
+    name: MIT`,
+  }
+
+  return createBaseEntity<ApiEntitySpec>(
+    "API",
+    params,
+    (p: EntityGeneratorParams<ApiEntitySpec>) => ({
+      ...defaultSpec,
+      ...p.spec
+    })
+  );
+};
diff --git a/src/generators/kinds/component.ts b/src/generators/kinds/component.ts
new file mode 100644
index 0000000..9d2175d
--- /dev/null
+++ b/src/generators/kinds/component.ts
@@ -0,0 +1,36 @@
+import { faker } from "@faker-js/faker";
+import { Entity } from "../../types";
+import { EntityGeneratorParams, WellKnownLifecycle, BaseEntitySpec } from "../types";
+import { createBaseEntity } from "../createBaseEntity";
+
+export type WellKnownComponentType = 'website' | 'service' | 'library';
+export interface ComponentEntitySpec extends BaseEntitySpec {
+  type: WellKnownComponentType;
+  lifecycle: WellKnownLifecycle;
+  owner: string;
+  system?: string;
+  subcomponentOf?: string;
+  providesApis?: string[];
+  consumesApis?: string[];
+  dependsOn?: string[];
+  dependencyOf?: string[];
+}
+
+export const generateComponentEntity = (params: EntityGeneratorParams<ComponentEntitySpec>): Entity => {
+  const componentTypes = ["service", "website", "library"];
+  const lifecycleStates = ["experimental", "production", "deprecated"];
+
+  const defaultSpec = {
+    type: componentTypes[Math.floor(Math.random() * componentTypes.length)],
+    lifecycle: lifecycleStates[0]
+  };
+
+  return createBaseEntity<ComponentEntitySpec>(
+    "Component",
+    params,
+    (p: EntityGeneratorParams<ComponentEntitySpec>) => ({
+      ...defaultSpec,
+      ...p.spec
+    })
+  );
+};
diff --git a/src/generators/kinds/domain.ts b/src/generators/kinds/domain.ts
new file mode 100644
index 0000000..5659b28
--- /dev/null
+++ b/src/generators/kinds/domain.ts
@@ -0,0 +1,18 @@
+import { Entity } from "../../types";
+import { BaseEntitySpec, EntityGeneratorParams } from "../types";
+import { createBaseEntity } from "../createBaseEntity";
+export interface DomainEntitySpec extends BaseEntitySpec {
+  owner: string;
+  subdomainOf?: string;
+  type?: string;
+}
+
+export const generateDomainEntity = (params: EntityGeneratorParams<DomainEntitySpec>): Entity => {
+  return createBaseEntity<DomainEntitySpec>(
+    "Domain",
+    params,
+    (p: EntityGeneratorParams<DomainEntitySpec>) => ({
+      ...p.spec
+    })
+  );
+};
diff --git a/src/generators/kinds/group.ts b/src/generators/kinds/group.ts
new file mode 100644
index 0000000..a3ff170
--- /dev/null
+++ b/src/generators/kinds/group.ts
@@ -0,0 +1,35 @@
+import { Entity } from "../../types";
+import { BaseEntitySpec, EntityGeneratorParams, EntityProfile } from "../types";
+import { createBaseEntity } from "../createBaseEntity";
+export interface GroupEntitySpec extends BaseEntitySpec {
+  type: string;
+  profile?: EntityProfile;
+  parent?: string;
+  children: string[];
+  members: string[];
+}
+
+export const generateGroupEntity = (params: EntityGeneratorParams<GroupEntitySpec>): Entity => {
+  const { name } = params;
+  const defaultSpec = {
+    type: "team",
+    profile: {
+      displayName: `${name} Team`,
+      email: `${name}@example.com`,
+      picture: `https://example.com/avatar/${name}.png`
+    },
+    parent: 'team-a',
+    children: [],
+    members: []
+  };
+
+  return createBaseEntity<GroupEntitySpec>(
+    "Group",
+    params,
+    (p: EntityGeneratorParams<GroupEntitySpec>) => ({
+      ...defaultSpec,
+      ...p.spec,
+      owner: undefined
+    })
+  );
+};
diff --git a/src/generators/kinds/location.ts b/src/generators/kinds/location.ts
new file mode 100644
index 0000000..23140ea
--- /dev/null
+++ b/src/generators/kinds/location.ts
@@ -0,0 +1,33 @@
+import { Entity } from "../../types";
+import { createBaseEntity } from "../createBaseEntity";
+import { EntityGeneratorParams } from "../types";
+export interface LocationEntitySpec {
+  type?: string;
+  target?: string;
+  targets?: string[];
+  presence?: "required" | "optional";
+}
+
+export const generateLocationEntity = (params: EntityGeneratorParams<LocationEntitySpec>): Entity => {
+  const { name } = params;
+  const targetSpec: { target?: string; targets?: string[] } = {};
+
+  if (params.spec?.target) {
+    targetSpec.target = params.spec.target;
+  } else if (params.spec?.targets && params.spec.targets.length > 0) {
+    targetSpec.targets = params.spec.targets;
+  } else {
+    targetSpec.targets = [
+      `http://github.com/myorg/myproject/${name}/catalog-info-1.yaml`,
+      `http://github.com/myorg/myproject/${name}/catalog-info-2.yaml`
+    ];
+  }
+
+  return createBaseEntity<LocationEntitySpec>(
+    "Location",
+    params,
+    (p: EntityGeneratorParams<LocationEntitySpec>) => ({
+      ...p.spec
+    })
+  );
+};
diff --git a/src/generators/kinds/resource.ts b/src/generators/kinds/resource.ts
new file mode 100644
index 0000000..73dba56
--- /dev/null
+++ b/src/generators/kinds/resource.ts
@@ -0,0 +1,28 @@
+import { Entity } from "../../types";
+import { BaseEntitySpec, EntityGeneratorParams } from "../types";
+import { createBaseEntity } from "../createBaseEntity";
+
+export interface ResourceEntitySpec extends BaseEntitySpec {
+  type: string;
+  owner: string;
+  system?: string;
+  dependsOn?: string[];
+  dependencyOf?: string[];
+}
+
+export const generateResourceEntity = (params: EntityGeneratorParams<ResourceEntitySpec>): Entity => {
+  const defaultSpec = {
+    system: params.spec?.system,
+    dependsOn: params.spec?.dependsOn,
+    dependencyOf: params.spec?.dependencyOf
+  };
+
+  return createBaseEntity<ResourceEntitySpec>(
+    "Resource",
+    params,
+    (p: EntityGeneratorParams<ResourceEntitySpec>) => ({
+      ...defaultSpec,
+      ...p.spec
+    })
+  );
+};
diff --git a/src/generators/kinds/system.ts b/src/generators/kinds/system.ts
new file mode 100644
index 0000000..c82c008
--- /dev/null
+++ b/src/generators/kinds/system.ts
@@ -0,0 +1,19 @@
+import { Entity } from "../../types";
+import { BaseEntitySpec, EntityGeneratorParams } from "../types";
+import { createBaseEntity } from "../createBaseEntity";
+
+export interface SystemEntitySpec extends BaseEntitySpec {
+  owner: string;
+  domain?: string;
+  type?: string;
+}
+
+export const generateSystemEntity = (params: EntityGeneratorParams<SystemEntitySpec>): Entity => {
+  return createBaseEntity<SystemEntitySpec>(
+    "System",
+    params,
+    (p: EntityGeneratorParams<SystemEntitySpec>) => ({
+      ...p.spec
+    })
+  );
+};
diff --git a/src/generators/kinds/template.ts b/src/generators/kinds/template.ts
new file mode 100644
index 0000000..41401c3
--- /dev/null
+++ b/src/generators/kinds/template.ts
@@ -0,0 +1,90 @@
+import { Entity } from "../../types";
+import { createBaseEntity } from "../createBaseEntity";
+import { EntityGeneratorParams } from "../types";
+export interface TemplateStep {
+  id: string;
+  name: string;
+  action: string;
+  input: Record<string, unknown>;
+}
+export interface TemplateParameter {
+  title: string;
+  required?: string[];
+  properties: Record<string, {
+    title?: string;
+    type?: string;
+    description?: string;
+    [key: string]: unknown;
+  }>;
+}
+export interface TemplateEntitySpec {
+  owner: string;
+  type: string;
+  parameters: TemplateParameter[];
+  steps: TemplateStep[];
+}
+
+export const generateTemplateEntity = (params: EntityGeneratorParams<TemplateEntitySpec>): Entity => {
+  const defaultParameters = [
+    {
+      title: "Fill in details",
+      required: ["name"],
+      properties: {
+        name: {
+          title: "Name",
+          type: "string",
+          description: "Unique name of the component"
+        }
+      }
+    },
+    {
+      title: "Choose a location",
+      required: ["repoUrl"],
+      properties: {
+        repoUrl: {
+          title: "Repository Location",
+          type: "string"
+        }
+      }
+    }
+  ];
+
+  // Default template steps
+  const defaultSteps = [
+    {
+      id: "fetch-base",
+      name: "Fetch Base",
+      action: "fetch:template",
+      input: {
+        url: "./template",
+        values: {
+          name: '{{ parameters.name }}'
+        }
+      }
+    },
+    {
+      id: "publish",
+      name: "Publish",
+      action: "publish:github",
+      input: {
+        allowedHosts: ['github.com'],
+        description: 'This is {{ parameters.name }}',
+        repoUrl: '{{ parameters.repoUrl }}'
+      }
+    }
+  ];
+
+  const defaultSpec = {
+    parameters: defaultParameters,
+    steps: defaultSteps
+  };
+
+  return createBaseEntity<TemplateEntitySpec>(
+    "Template",
+    params,
+    (p: EntityGeneratorParams<TemplateEntitySpec>) => ({
+      ...defaultSpec,
+      ...p.spec
+    })
+  );
+};
diff --git a/src/generators/kinds/user.ts b/src/generators/kinds/user.ts
new file mode 100644
index 0000000..3828a22
--- /dev/null
+++ b/src/generators/kinds/user.ts
@@ -0,0 +1,30 @@
+import { faker } from "@faker-js/faker";
+import { Entity } from "../../types";
+import { EntityGeneratorParams, EntityProfile } from "../types";
+import { createBaseEntity } from "../createBaseEntity";
+export interface UserEntitySpec {
+  profile?: EntityProfile;
+  memberOf: string[];
+}
+
+export const generateUserEntity = (params: EntityGeneratorParams<UserEntitySpec>): Entity => {
+  const { name } = params;
+  const defaultSpec = {
+    profile: {
+      displayName: params.spec?.profile?.displayName || faker.person.fullName(),
+      email: params.spec?.profile?.email || `${name}@example.com`,
+      picture: params.spec?.profile?.picture || 'https://avatars.githubusercontent.com/u/55416270'
+    },
+    memberOf: params.spec?.memberOf || []
+  };
+
+  return createBaseEntity<UserEntitySpec>(
+    "User",
+    params,
+    (p: EntityGeneratorParams<UserEntitySpec>) => ({
+      ...defaultSpec,
+      ...p.spec,
+      owner: undefined
+    })
+  );
+};
diff --git a/src/generators/registry.ts b/src/generators/registry.ts
new file mode 100644
index 0000000..fb3d4dd
--- /dev/null
+++ b/src/generators/registry.ts
@@ -0,0 +1,43 @@
+import { Entity } from "../types";
+import { generateApiEntity } from "./kinds/api";
+import { generateComponentEntity } from "./kinds/component";
+import { generateDomainEntity } from "./kinds/domain";
+import { generateGroupEntity } from "./kinds/group";
+import { generateLocationEntity } from "./kinds/location";
+import { generateSystemEntity } from "./kinds/system";
+import { generateTemplateEntity } from "./kinds/template";
+import { generateUserEntity } from "./kinds/user";
+import { generateResourceEntity } from "./kinds/resource";
+import { EntityGeneratorParams, EntityGenerator, EntitySpecMap } from "./types";
+
+/**
+ * Registry of entity generators
+ */
+export const generators: {
+  [K in keyof EntitySpecMap]: EntityGenerator<EntitySpecMap[K]>
+} = {
+  API: generateApiEntity,
+  Component: generateComponentEntity,
+  Domain: generateDomainEntity,
+  Group: generateGroupEntity,
+  User: generateUserEntity,
+  Resource: generateResourceEntity,
+  System: generateSystemEntity,
+  Location: generateLocationEntity,
+  Template: generateTemplateEntity
+};
+
+/**
+ * Generates an entity of a given kind
+ */
+export const generateEntity = <K extends keyof EntitySpecMap>(
+  kind: K,
+  params: EntityGeneratorParams<EntitySpecMap[K]>
+): Entity => {
+  const generator = generators[kind];
+  if (!generator) {
+    throw new Error(`Unknown entity kind: ${kind}. Available kinds: ${Object.keys(generators).join(', ')}`);
+  }
+
+  return generator(params);
+};
diff --git a/src/generators/types.ts b/src/generators/types.ts
new file mode 100644
index 0000000..e02eac5
--- /dev/null
+++ b/src/generators/types.ts
@@ -0,0 +1,45 @@
+import { Entity } from "../types";
+import { ApiEntitySpec } from "./kinds/api";
+import { ComponentEntitySpec } from "./kinds/component";
+import { DomainEntitySpec } from "./kinds/domain";
+import { GroupEntitySpec } from "./kinds/group";
+import { LocationEntitySpec } from "./kinds/location";
+import { ResourceEntitySpec } from "./kinds/resource";
+import { SystemEntitySpec } from "./kinds/system";
+import { TemplateEntitySpec } from "./kinds/template";
+import { UserEntitySpec } from "./kinds/user";
+
+export interface EntityProfile {
+  displayName?: string;
+  email?: string;
+  picture?: string;
+}
+
+export type WellKnownLifecycle = 'experimental' | 'production' | 'deprecated';
+
+export interface BaseEntitySpec {
+  type?: string;
+  owner: string;
+  [key: string]: unknown;
+}
+
+export type EntitySpecMap = {
+  API: ApiEntitySpec;
+  Component: ComponentEntitySpec;
+  Domain: DomainEntitySpec;
+  Group: GroupEntitySpec;
+  Location: LocationEntitySpec;
+  Resource: ResourceEntitySpec;
+  System: SystemEntitySpec;
+  Template: TemplateEntitySpec;
+  User: UserEntitySpec;
+}
+
+export interface EntityGeneratorParams<T> {
+  name: string;
+  owner?: string;
+  annotations?: Record<string, string>;
+  spec?: Partial<T>;
+}
+
+export type EntityGenerator<T> = (params: EntityGeneratorParams<T>) => Entity;
diff --git a/src/types.ts b/src/types.ts
index 370f9cf..dda68eb 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1 +1,23 @@
-export type EntityType = "Component" | "API" | "System" | "Domain" | "Resource";
+export type EntityKind = 'API' | 'Component' | 'Domain' | 'Group' | 'Location' | 'Resource' | 'System' | 'Template' | 'User';
+export interface Entity {
+  apiVersion: string;
+  kind: EntityKind;
+  metadata: EntityMetadata;
+  spec: Record<string, unknown>;
+}
+
+export interface EntityLink {
+  url: string;
+  title?: string;
+  icon?: string;
+  type?: string;
+}
+
+export interface EntityMetadata {
+  name: string;
+  description?: string;
+  annotations?: Record<string, string>;
+  labels?: Record<string, string>;
+  tags?: string[];
+  links?: EntityLink[];
+}
diff --git a/src/utils/generateEntities.ts b/src/utils/generateEntities.ts
index 86ad18c..d74e522 100644
--- a/src/utils/generateEntities.ts
+++ b/src/utils/generateEntities.ts
@@ -1,30 +1,10 @@
-import { EntityType } from "../types";
 import yaml from "js-yaml";
 import { faker } from "@faker-js/faker";
-
-interface EntityMetadata {
-  name: string;
-  description: string;
-  annotations?: Record<string, string>;
-}
-
-interface EntitySpec {
-  type?: string;
-  lifecycle?: string;
-  owner: string;
-  definition?: string;
-  domain?: string;
-}
-
-interface Entity {
-  apiVersion: string;
-  kind: string;
-  metadata: EntityMetadata;
-  spec: EntitySpec;
-}
+import { generateEntity } from "../generators/registry";
+import { EntityKind, Entity } from "../types";
 
 export const generateEntities = (
-  entityType: EntityType,
+  entityKind: EntityKind,
   amount: number,
   owner?: string | null,
   annotations?: Record<string, string>
@@ -32,66 +12,14 @@ export const generateEntities = (
   const entities: Entity[] = [];
 
   for (let i = 0; i < amount; i++) {
-    const randomDescription = faker.commerce.productDescription();
-    const entityName = `example-${faker.hacker.abbreviation().toLowerCase()}-${
-      i + 1
-    }`;
-    const randomType = ["website", "service", "library"];
+    const entityName = `example-${faker.hacker.abbreviation().toLowerCase()}-${i + 1}`;
 
-    const metadata: EntityMetadata = {
+    const entity = generateEntity(entityKind, {
       name: entityName,
-      description: randomDescription,
-    };
-
-    if (annotations && Object.keys(annotations).length > 0) {
-      metadata.annotations = {
-        ...annotations,
-      };
-    }
-
-    const entity: Entity = {
-      apiVersion: "backstage.io/v1alpha1",
-      kind: entityType,
-      metadata: metadata,
-      spec: {
-        owner: owner ?? "team-a",
-      },
-    };
-
-    if (entityType === "Component") {
-      entity.spec = {
-        type: randomType[Math.floor(Math.random() * randomType.length)],
-        lifecycle: "experimental",
-        owner: owner ?? "team-a",
-      };
-    } else if (entityType === "API") {
-      entity.spec = {
-        type: "openapi",
-        lifecycle: "production",
-        owner: owner ?? "team-b",
-        definition: `openapi: "3.0.0"
-info:
-  version: 1.0.0
-  title: ${entityName}
-  license:
-    name: MIT`,
-      };
-    } else if (entityType === "System") {
-      entity.spec = {
-        owner: owner ?? "team-b",
-        domain: `${faker.hacker
-          .noun()
-          .toLowerCase()
-          .split(" ")
-          .join("-")}-domain`,
-      };
-    } else if (entityType === "Resource") {
-      entity.spec = {
-        type: faker.database.engine().toLowerCase(),
-        lifecycle: "experimental",
-        owner: owner ?? "team-a",
-      };
-    }
+      owner: owner || undefined,
+      annotations,
+      spec: {}  // todo: pass any custom spec options if needed
+    });
 
     entities.push(entity);
   }
@@ -99,7 +27,8 @@ info:
   const yamlDocuments = entities.map((entity) =>
     yaml.dump(entity, { lineWidth: -1 })
   );
-  const result = `# This file contains ${amount} Backstage ${entityType} entities\n\n${yamlDocuments.join(
+
+  const result = `# This file contains ${amount} Backstage ${entityKind} entities\n\n${yamlDocuments.join(
     "---\n"
   )}`;
 


Reviews:
user: login: Parsifal-M
body: Heya! 👋 

Love this! Thanks so much for putting the time and effort in.

Just a couple of comments, please let me know if you want some help to get it over the finish line as you've done loads already 😄 🚀 
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-03-18T07:45:55Z
user: login: kurtaking
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T14:18:36Z
user: login: kurtaking
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T14:55:05Z
user: login: Parsifal-M
state: APPROVED
author_association: OWNER
submitted_at: 2025-03-18T15:09:57Z

Review comments:
diff_hunk: @@ -0,0 +1,45 @@
+import { Entity } from "../types";
+import { ApiEntitySpec } from "./kinds/api";
+import { ComponentEntitySpec } from "./kinds/component";
+import { DomainEntitySpec } from "./kinds/domain";
+import { GroupEntitySpec } from "./kinds/group";
+import { LocationEntitySpec } from "./kinds/location";
+import { ResourceEntitySpec } from "./kinds/resource";
+import { SystemEntitySpec } from "./kinds/system";
+import { TemplateEntitySpec } from "./kinds/template";
+import { UserEntitySpec } from "./kinds/user";
+
+export interface EntityProfile {
+  displayName?: string;
+  email?: string;
+  picture?: string;
+}
+
+export type WellKnownLifecycle = 'experimental' | 'production' | 'deprecated';
+
+export interface BaseEntitySpec {
+  type?: string;
+  owner: string;
path: src/generators/types.ts
user: login: Parsifal-M
body: _Bit of a pain_ but I think we need to hide the (or disable) the owner field in the form if the kind is one of user, group, location 👍 
created_at: 2025-03-18T07:41:19Z
author_association: OWNER
diff_hunk: @@ -1,105 +1,34 @@
-import { EntityType } from "../types";
 import yaml from "js-yaml";
 import { faker } from "@faker-js/faker";
-
-interface EntityMetadata {
-  name: string;
-  description: string;
-  annotations?: Record<string, string>;
-}
-
-interface EntitySpec {
-  type?: string;
-  lifecycle?: string;
-  owner: string;
-  definition?: string;
-  domain?: string;
-}
-
-interface Entity {
-  apiVersion: string;
-  kind: string;
-  metadata: EntityMetadata;
-  spec: EntitySpec;
-}
+import { generateEntity } from "../generators/registry";
+import { EntityKind, Entity } from "../types";
 
 export const generateEntities = (
-  entityType: EntityType,
+  entityKind: EntityKind,
   amount: number,
   owner?: string | null,
   annotations?: Record<string, string>
 ): string => {
   const entities: Entity[] = [];
 
   for (let i = 0; i < amount; i++) {
-    const randomDescription = faker.commerce.productDescription();
-    const entityName = `example-${faker.hacker.abbreviation().toLowerCase()}-${
-      i + 1
-    }`;
-    const randomType = ["website", "service", "library"];
+    const entityName = `example-${faker.hacker.abbreviation().toLowerCase()}-${i + 1}`;
 
-    const metadata: EntityMetadata = {
+    const entity = generateEntity(entityKind, {
       name: entityName,
-      description: randomDescription,
-    };
-
-    if (annotations && Object.keys(annotations).length > 0) {
-      metadata.annotations = {
-        ...annotations,
-      };
-    }
path: src/utils/generateEntities.ts
user: login: Parsifal-M
body: Think we need something like this (dealers choice) so we don't end up with:

```yaml
metadata:
  name: example-adp-1
  description: Featuring Argon-enhanced technology, our Computer offers unparalleled aware performance
  annotations: {} # If none are provided lets just remove the annotations (or possibly default to one)
```
created_at: 2025-03-18T07:43:35Z
author_association: OWNER
diff_hunk: @@ -1,105 +1,34 @@
-import { EntityType } from "../types";
 import yaml from "js-yaml";
 import { faker } from "@faker-js/faker";
-
-interface EntityMetadata {
-  name: string;
-  description: string;
-  annotations?: Record<string, string>;
-}
-
-interface EntitySpec {
-  type?: string;
-  lifecycle?: string;
-  owner: string;
-  definition?: string;
-  domain?: string;
-}
-
-interface Entity {
-  apiVersion: string;
-  kind: string;
-  metadata: EntityMetadata;
-  spec: EntitySpec;
-}
+import { generateEntity } from "../generators/registry";
+import { EntityKind, Entity } from "../types";
 
 export const generateEntities = (
-  entityType: EntityType,
+  entityKind: EntityKind,
   amount: number,
   owner?: string | null,
   annotations?: Record<string, string>
 ): string => {
   const entities: Entity[] = [];
 
   for (let i = 0; i < amount; i++) {
-    const randomDescription = faker.commerce.productDescription();
-    const entityName = `example-${faker.hacker.abbreviation().toLowerCase()}-${
-      i + 1
-    }`;
-    const randomType = ["website", "service", "library"];
+    const entityName = `example-${faker.hacker.abbreviation().toLowerCase()}-${i + 1}`;
 
-    const metadata: EntityMetadata = {
+    const entity = generateEntity(entityKind, {
       name: entityName,
-      description: randomDescription,
-    };
-
-    if (annotations && Object.keys(annotations).length > 0) {
-      metadata.annotations = {
-        ...annotations,
-      };
-    }
path: src/utils/generateEntities.ts
user: login: kurtaking
body: This is fixed. We are now checking if the value is undefined and if not, has at least one key prior to adding annotations. 
created_at: 2025-03-18T14:18:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,45 @@
+import { Entity } from "../types";
+import { ApiEntitySpec } from "./kinds/api";
+import { ComponentEntitySpec } from "./kinds/component";
+import { DomainEntitySpec } from "./kinds/domain";
+import { GroupEntitySpec } from "./kinds/group";
+import { LocationEntitySpec } from "./kinds/location";
+import { ResourceEntitySpec } from "./kinds/resource";
+import { SystemEntitySpec } from "./kinds/system";
+import { TemplateEntitySpec } from "./kinds/template";
+import { UserEntitySpec } from "./kinds/user";
+
+export interface EntityProfile {
+  displayName?: string;
+  email?: string;
+  picture?: string;
+}
+
+export type WellKnownLifecycle = 'experimental' | 'production' | 'deprecated';
+
+export interface BaseEntitySpec {
+  type?: string;
+  owner: string;
path: src/generators/types.ts
user: login: kurtaking
body: We now override the owner field in a group or user spec, along with hiding the owner text field when a group or user entity is selected to be generated. 
created_at: 2025-03-18T14:55:05Z
author_association: CONTRIBUTOR

PR comments:
user: login: kurtaking
created_at: 2025-03-17T15:37:24Z
body:  👋🏼 There is some duplication that can be consolidated, but otherwise, ready for a review.
author_association: CONTRIBUTOR
user: login: Parsifal-M
created_at: 2025-03-17T18:19:28Z
body: >  👋🏼 There is some duplication that can be consolidated, but otherwise, ready for a review.

Awesome! Tyvm! Will take a look tomorrow and we can ship!
author_association: OWNER
user: login: kurtaking
created_at: 2025-03-18T14:56:15Z
body: Alright, I think we are good to go on existing feedback. 
author_association: CONTRIBUTOR
user: login: Parsifal-M
created_at: 2025-03-18T15:09:40Z
body: Love it! Lets :shipit: 
author_association: OWNER