Skip to main content

Entities & Components

This page is your reference for BunSane's data model: how to define components, create entities, query data, and use transactions.

Components

Components are data containers that you attach to entities. Each component type becomes its own table in PostgreSQL.

Defining Components

Create a component by extending BaseComponent and marking it with @Component:

import { BaseComponent, CompData, Component } from "bunsane/core/components";

@Component
export class NameComponent extends BaseComponent {
@CompData()
value: string = "";
}

@Component
export class EmailComponent extends BaseComponent {
@CompData()
value: string = "";

@CompData()
verified: boolean = false;
}

The @CompData Decorator

Fields marked with @CompData() are persisted to the database. Fields without it are ignored.

@Component
export class ProfileComponent extends BaseComponent {
@CompData()
firstName: string = "";

@CompData()
lastName: string = "";

@CompData({ indexed: true })
username: string = "";

@CompData()
createdAt: Date = new Date();
}

Options

OptionTypeDescription
indexedbooleanCreates a database index on this field for faster queries
nullablebooleanAllows the field to be null in the database

Use { indexed: true } on fields you frequently filter or sort by.

Component Methods

Components can include helper methods:

@Component
export class PasswordComponent extends BaseComponent {
@CompData()
value: string = "";

static async makeHash(plaintext: string): Promise<string> {
return await Bun.password.hash(plaintext);
}
}

Tags

A Tag is a component with no data fields. Tags are used to label entities by type:

@Component
export class UserTag extends BaseComponent {}

@Component
export class OrderTag extends BaseComponent {}

Tags are essential for queries -- they let you find "all users" or "all orders" without needing to know which specific data components are attached.

Entities

An entity is a container that holds components. Each entity has a unique UUID and can have any combination of components attached.

Creating Entities

Use Entity.Create() to create a new entity and chain .add() calls to attach components:

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

const user = Entity.Create()
.add(UserTag, {})
.add(NameComponent, { value: "John Doe" })
.add(EmailComponent, { value: "john@example.com", verified: false });

await user.save();

The entity is not written to the database until you call .save().

Finding Entities

Look up an entity by its ID:

const user = await Entity.FindById("entity-uuid-here");

Returns null if no entity with that ID exists.

Reading Component Data

Get a component's data from an entity:

const nameComp = await entity.get(NameComponent);
console.log(nameComp?.value); // "John Doe"

The result is null if the entity does not have that component.

Updating Component Data

Update an existing component:

await entity.set(NameComponent, { value: "Jane Doe" });
await entity.save();

Adding Components

Attach a new component to an existing entity:

entity.add(ProfilePictureComponent, { path: "/uploads/avatar.jpg" });
await entity.save();

Deleting Entities

await entity.delete();

Saving

Always call save() after making changes. Without it, changes stay in memory only:

await entity.save();

Querying Entities

The Query class lets you find entities based on which components they have and what data those components contain.

import { Query } from "bunsane/query";

Basic Query

Find all entities that have a specific component:

const users = await new Query()
.with(EmailComponent)
.exec();

Filtering

Filter by component field values using Query.filters() and Query.filter():

const users = await new Query()
.with(
EmailComponent,
Query.filters(
Query.filter("value", Query.filterOp.EQ, "john@example.com")
)
)
.exec();

You can combine multiple filters on the same component:

const devices = await new Query()
.with(
UserDeviceComponent,
Query.filters(
Query.filter("device.unique_id", Query.filterOp.EQ, deviceId),
Query.filter("verified", Query.filterOp.EQ, true)
)
)
.exec();

Filter Operators

All available operators in Query.filterOp:

OperatorDescription
EQEquals
NEQNot equals
GTGreater than
GTEGreater than or equal
LTLess than
LTELess than or equal
LIKEPattern matching (SQL LIKE)
INValue is in array
NOT_INValue is not in array

Multiple Components

Require entities to have several components:

const results = await new Query()
.with(UserTag)
.with(EmailComponent)
.exec();

Excluding Components

Find entities that do not have a component:

const unverified = await new Query()
.with(UserTag)
.without(VerifiedTag)
.exec();

Pagination and Sorting

const users = await new Query()
.with(ProfileComponent)
.sortBy(ProfileComponent, "createdAt", "DESC")
.take(10)
.offset(20)
.exec();
  • .sortBy(Component, "field", "ASC" | "DESC") -- sort results
  • .take(n) -- limit to n results
  • .offset(n) -- skip the first n results

Counting Results

Get a count without loading all entities:

const count = await new Query()
.with(OrderTag)
.count();

Transactions

When you need multiple database operations to succeed or fail together, wrap them in a transaction:

import db from "bunsane/database";

const result = await db.transaction(async (trx) => {
// All operations inside use the same transaction
await device.set(UserDeviceComponent, {
otp_code: "",
verified: true,
}, { trx });
await device.save(trx);

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

const phone = await user.get(PhoneComponent, { trx });
if (phone) {
await user.set(PhoneComponent, { ...phone, verified: true }, { trx });
await user.save(trx);
}

return user;
});

Pass the trx object to every entity operation inside the transaction. If any operation throws, the entire transaction is rolled back.