Skip to main content

Archetypes

An archetype groups related components together into a named shape -- like saying "a User is an entity with a name, an email, and a phone number." Archetypes also auto-generate GraphQL types, so you do not have to write schema definitions by hand.

Why Archetypes?

Without archetypes, you would need to manually specify which components to include every time you return data from a GraphQL operation. Archetypes solve this by giving a name and structure to common entity shapes.

When you define a User archetype, BunSane automatically:

  • Creates a User GraphQL type with fields matching your components
  • Creates a UserInput GraphQL input type for mutations
  • Provides helper methods for creating and updating entities

Defining an Archetype

import {
ArcheType,
ArcheTypeField,
BaseArcheType,
type ArcheTypeOwnProperties,
} from "bunsane/core/ArcheType";
import { NameComponent, EmailComponent, PhoneComponent } from "./components/UserComponent";

@ArcheType("User")
export class UserArcheTypeClass extends BaseArcheType {
@ArcheTypeField(NameComponent)
name!: NameComponent;

@ArcheTypeField(EmailComponent, { nullable: true })
email!: EmailComponent;

@ArcheTypeField(PhoneComponent)
phone!: PhoneComponent;
}

export type IUserArcheType = ArcheTypeOwnProperties<UserArcheTypeClass>;
export const UserArcheType = new UserArcheTypeClass();

A few things to note:

  • @ArcheType("User") registers the archetype with the name "User" -- this becomes the GraphQL type name
  • @ArcheTypeField(Component) maps a component to a field on the archetype
  • { nullable: true } marks a field as optional (it may not be present on every entity)
  • Export a type using ArcheTypeOwnProperties<T> for type inference in your services
  • Export an instance of the archetype for use in service decorators and operations

The @ArcheType Decorator

The decorator takes a name string that becomes the GraphQL type name:

@ArcheType("User")
class UserArcheTypeClass extends BaseArcheType { }

// You can also use an enum for consistency across your codebase
enum ArchetypeKind {
User = "User",
Order = "Order",
}

@ArcheType(ArchetypeKind.User)
class UserArcheTypeClass extends BaseArcheType { }

The @ArcheTypeField Decorator

Each field maps a component to the archetype:

@ArcheType("Product")
export class ProductArcheTypeClass extends BaseArcheType {
@ArcheTypeField(ProductInfoComponent)
info!: ProductInfoComponent;

@ArcheTypeField(PricingComponent, { nullable: true })
pricing!: PricingComponent;

@ArcheTypeField(InventoryComponent, { nullable: true })
inventory!: InventoryComponent;
}

Use { nullable: true } for components that may not exist on every entity of this type.

Computed Fields (@ArcheTypeFunction)

Add fields that are calculated at query time using @ArcheTypeFunction. These appear in the GraphQL schema as regular fields but are resolved by calling a method on the archetype class instead of reading from a component.

Basic Usage

Decorate a method with @ArcheTypeFunction and provide a returnType. The method receives the resolved Entity and returns a computed value.

import { ArcheType, ArcheTypeField, ArcheTypeFunction, BaseArcheType } from "bunsane/core/ArcheType";
import { Entity } from "bunsane/core/Entity";

@ArcheType("Customer")
export class CustomerArcheTypeClass extends BaseArcheType {
@ArcheTypeField(PersonNameComponent)
name!: PersonNameComponent;

@ArcheTypeField(MembershipComponent, { nullable: true })
membership!: MembershipComponent;

@ArcheTypeFunction({ returnType: "String" })
async display_name(entity: Entity) {
const name = await entity.get(PersonNameComponent);
if (!name) return "";
const { firstName, lastName, title } = name;
return [title, firstName, lastName].filter(Boolean).join(" ");
}

@ArcheTypeFunction({ returnType: "Boolean" })
async is_premium(entity: Entity) {
const membership = await entity.get(MembershipComponent);
if (!membership) return false;
return membership.tier === "gold" || membership.tier === "platinum";
}
}

Return Types

The returnType option is a GraphQL scalar or type name string. It is inserted verbatim into the generated schema, so the value must be a valid GraphQL type name.

returnType valueGraphQL type
"String"String
"Int"Int
"Float"Float
"Boolean"Boolean
"Point" (or any custom name)Inserted verbatim into the schema

Always specify returnType explicitly. If omitted, BunSane reflects the TypeScript return type -- but async methods reflect as Promise, which falls back to Any in the generated schema.

Arguments

@ArcheTypeFunction supports GraphQL arguments via the args option. Each entry specifies a name, a type constructor, and an optional nullable flag.

import { ArcheTypeFunction } from "bunsane/core/ArcheType";
import { Entity } from "bunsane/core/Entity";

@ArcheTypeFunction({
returnType: "Float",
args: [
{ name: "unit", type: String, nullable: true },
],
})
async distance_to(entity: Entity, unit?: string) {
const location = await entity.get(LocationComponent);
if (!location) return null;
const factor = unit === "miles" ? 0.621371 : 1;
return location.distanceKm * factor;
}

Argument type values map to GraphQL types as follows:

type valueGraphQL type
StringString
NumberFloat
BooleanBoolean
DateDate
Custom classResolved from the type registry

Arguments with nullable: false (the default) are required -- the resolver throws if the argument is missing. Set nullable: true for optional arguments.

How It Differs from @ArcheTypeField

@ArcheTypeField@ArcheTypeFunction
SourceECS component data from DB/cacheMethod on the archetype class
GraphQL inputIncluded in the generated input typeExcluded from input types
ArgumentsNoneSupported via args option
DataLoaderBatched automaticallyCalls method directly per entity

Registering Resolvers

For @ArcheTypeFunction (and relations) to resolve in GraphQL, call registerFieldResolvers() in your service constructor. See Registering Field Resolvers below.

Generated Schema Example

This archetype definition:

import { ArcheType, ArcheTypeField, ArcheTypeFunction, BaseArcheType } from "bunsane/core/ArcheType";
import { Entity } from "bunsane/core/Entity";

@ArcheType("Store")
export class StoreArcheTypeClass extends BaseArcheType {
@ArcheTypeField(StoreInfoComponent)
info!: StoreInfoComponent;

@ArcheTypeFunction({ returnType: "Boolean" })
async is_open(entity: Entity) { ... }

@ArcheTypeFunction({
returnType: "Float",
args: [{ name: "unit", type: String, nullable: true }],
})
async distance_to(entity: Entity, unit?: string) { ... }
}

Generates this GraphQL schema:

type Store {
info: StoreInfoComponent!
is_open: Boolean
distance_to(unit: String): Float
}

input StoreInput {
info: StoreInfoComponentInput!
}

Computed fields do not appear in the input type -- only @ArcheTypeField fields are included in the generated StoreInput.

Relations

Archetypes support relationships between entity types.

HasOne

A one-to-one relationship:

import { HasOne } from "bunsane/core/ArcheType";

@ArcheType("User")
export class UserArcheTypeClass extends BaseArcheType {
@ArcheTypeField(ProfileComponent)
profile!: ProfileComponent;

@HasOne("Driver", { foreignKey: "user_id", nullable: true })
driver?: IDriverArcheType;
}

HasMany

A one-to-many relationship:

import { HasMany } from "bunsane/core/ArcheType";

@ArcheType("UserListResponse")
export class UserListResponseArcheTypeClass extends BaseArcheType {
@ArcheTypeField(CountTag)
totalData!: number;

@HasMany("User", { foreignKey: "id", nullable: true })
items!: IUserArcheType[];
}

BelongsTo

The inverse side of HasOne or HasMany:

import { BelongsTo } from "bunsane/core/ArcheType";

@ArcheType("UserDevice")
export class UserDeviceArcheTypeClass extends BaseArcheType {
@ArcheTypeField(UserDeviceComponent)
device!: UserDeviceComponent;

@BelongsTo("User", { foreignKey: "device.user_id" })
user!: IUserArcheType;
}

Relations use string identifiers (the archetype name) rather than direct class references.

Creating Entities with Archetypes

Use fill() and createEntity() for a shorthand way to create entities:

const user = UserArcheType.fill({
name: "John Doe",
phone: "+1234567890",
email: "john@example.com",
}).createEntity();

await user.save();

You can also add extra components after creating:

const user = UserArcheType.fill({
name: "John Doe",
phone: "+1234567890",
}).createEntity();

user.add(PhoneComponent, { value: "+1234567890", verified: false });
await user.save();

Updating Entities

Use updateEntity() to update specific components on an existing entity:

const user = await Entity.FindById(userId);
if (!user) throw new Error("User not found");

const updated = await UserArcheType.updateEntity(user, {
name: "Jane Doe",
});

await updated.save();

Input Schemas

Archetypes can generate Zod schemas for input validation:

// Full schema for the archetype
const schema = UserArcheType.getZodObjectSchema();

// Partial schema for update mutations
const updateSchema = UserArcheType.getInputSchema().partial().pick({
name: true,
});

These schemas are useful as inputs to @GraphQLOperation decorators (see Services).

Registering Field Resolvers

For computed fields (@ArcheTypeFunction) and relations to work in GraphQL, register the archetype's field resolvers in your service:

class UserService extends BaseService {
constructor(private app: App) {
super();
UserArcheType.registerFieldResolvers(this);
}
}

GraphQL Integration

The Customer archetype defined above automatically generates these GraphQL types:

type Customer {
name: PersonNameComponent!
membership: MembershipComponent
display_name: String
is_premium: Boolean
}

input CustomerInput {
name: PersonNameComponentInput!
membership: MembershipComponentInput
}

Use archetypes as the output type in your GraphQL operations:

@GraphQLOperation({
type: "Query",
output: UserArcheType,
})
async profile(args: {}, context: GraphQLContext) {
const userId = context.jwt.payload.user_id;
return await Entity.FindById(userId);
}