Skip to main content

Common Patterns

This guide provides reusable code patterns for common scenarios in BunSane applications.

Entity Patterns

Create Entity with Multiple Components

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

await user.save();

Create Using Archetype

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

await user.save();

Update Entity Components

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

// Get current values
const currentProfile = await user.get(ProfileComponent);

// Update with merged values
await user.set(ProfileComponent, {
...currentProfile,
bio: "Updated bio",
updatedAt: new Date(),
});

await user.save();

Entity Method Signatures

The get() and set() methods accept an optional context parameter for transactions and DataLoader integration:

// Method signatures
entity.get<T>(Component, context?: { loaders?: DataLoaders; trx?: Transaction }): Promise<T | null>
entity.set<T>(Component, data, context?: { loaders?: DataLoaders; trx?: Transaction }): Promise<this>

See the Transaction Patterns section for usage with transactions.

Update Using Archetype

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();

Add Component to Existing Entity

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

// Add new component
user.add(PremiumTag, {});
user.add(SubscriptionComponent, {
plan: "premium",
startDate: new Date(),
endDate: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000),
});

await user.save();

Check if Entity Has Component

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

const premiumTag = await user.get(PremiumTag);
const isPremium = premiumTag !== null;

Transaction Patterns

Basic Transaction

import db from "bunsane/database";

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

await user.set(BalanceComponent, { amount: 100 }, { trx });
await user.save(trx);
});

Transaction with Multiple Entities

const result = await db.transaction(async (trx) => {
// Debit source account
const source = await Entity.FindById(sourceId, trx);
const sourceBalance = await source.get(BalanceComponent, { trx });

if (sourceBalance.amount < amount) {
throw new Error("Insufficient funds");
}

await source.set(BalanceComponent, {
amount: sourceBalance.amount - amount,
}, { trx });
await source.save(trx);

// Credit destination account
const dest = await Entity.FindById(destId, trx);
const destBalance = await dest.get(BalanceComponent, { trx });

await dest.set(BalanceComponent, {
amount: destBalance.amount + amount,
}, { trx });
await dest.save(trx);

// Create transaction record
const txRecord = Entity.Create()
.add(TransactionTag, {})
.add(TransactionInfoComponent, {
sourceId,
destId,
amount,
timestamp: new Date(),
});
await txRecord.save(trx);

return txRecord.id;
});

Transaction with Rollback on Error

try {
await db.transaction(async (trx) => {
// Operations that might fail
await riskyOperation1(trx);
await riskyOperation2(trx);

// If any operation throws, entire transaction rolls back
});
} catch (error) {
// Transaction automatically rolled back
console.error("Transaction failed:", error);
throw error;
}

Query Patterns

Find Single by Unique Field

async function findUserByEmail(email: string): Promise<Entity | null> {
const results = await new Query()
.with(UserTag)
.with(
EmailComponent,
Query.filters(Query.filter("value", Query.filterOp.EQ, email))
)
.take(1)
.exec();

return results[0] || null;
}

Find with Multiple Conditions

async function findActiveUsersByRole(role: string) {
return await new Query()
.with(UserTag)
.with(
RoleComponent,
Query.filters(Query.filter("value", Query.filterOp.EQ, role))
)
.without(SoftDeletedTag)
.without(SuspendedTag)
.exec();
}

Paginated Query with Total Count

async function paginatedQuery<T>(
baseQuery: Query,
page: number,
pageSize: number
): Promise<{ items: Entity[]; total: number; pages: number }> {
const [items, total] = await Promise.all([
baseQuery
.take(pageSize)
.offset(page * pageSize)
.exec(),
baseQuery.count(),
]);

return {
items,
total,
pages: Math.ceil(total / pageSize),
};
}

// Usage
const result = await paginatedQuery(
new Query().with(UserTag).with(ProfileComponent),
0, // page
20 // pageSize
);

Date Range Query

async function findOrdersInDateRange(startDate: Date, endDate: Date) {
return await new Query()
.with(OrderTag)
.with(
OrderInfoComponent,
Query.filters(
Query.filter("createdAt", Query.filterOp.GTE, startDate),
Query.filter("createdAt", Query.filterOp.LTE, endDate)
)
)
.sortBy(OrderInfoComponent, "createdAt", "DESC")
.exec();
}

Search Query with Optional Filters

interface SearchFilters {
name?: string;
email?: string;
status?: string;
minPrice?: number;
maxPrice?: number;
}

async function searchProducts(filters: SearchFilters) {
let query = new Query().with(ProductTag);

if (filters.name) {
query = query.with(
ProductNameComponent,
Query.filters(
Query.filter("value", Query.filterOp.LIKE, `%${filters.name}%`)
)
);
}

if (filters.status) {
query = query.with(
ProductStatusComponent,
Query.filters(
Query.filter("value", Query.filterOp.EQ, filters.status)
)
);
}

if (filters.minPrice !== undefined || filters.maxPrice !== undefined) {
const priceFilters = [];
if (filters.minPrice !== undefined) {
priceFilters.push(
Query.filter("amount", Query.filterOp.GTE, filters.minPrice)
);
}
if (filters.maxPrice !== undefined) {
priceFilters.push(
Query.filter("amount", Query.filterOp.LTE, filters.maxPrice)
);
}
query = query.with(PriceComponent, Query.filters(...priceFilters));
}

return await query.take(100).exec();
}

Archetype Patterns

Basic Archetype Definition

import {
ArcheType,
ArcheTypeField,
BaseArcheType,
type ArcheTypeOwnProperties,
} from "bunsane/core/ArcheType";

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

@ArcheTypeField(EmailComponent)
email!: EmailComponent;

@ArcheTypeField(ProfileComponent, { nullable: true })
profile!: ProfileComponent;
}

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

Archetype with Computed Fields

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

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

@ArcheTypeField(ProfileComponent, { nullable: true })
profile!: ProfileComponent;

@ArcheTypeFunction({ returnType: "String" })
async displayName(entity: Entity) {
const name = await entity.get(NameComponent);
const profile = await entity.get(ProfileComponent);

if (profile?.nickname) {
return profile.nickname;
}
return name?.value || "Anonymous";
}

@ArcheTypeFunction({ returnType: "Boolean" })
async isComplete(entity: Entity) {
const name = await entity.get(NameComponent);
const email = await entity.get(EmailComponent);
const profile = await entity.get(ProfileComponent);

return !!(name?.value && email?.value && profile?.bio);
}
}

Archetype with Relations

import { HasOne, HasMany, BelongsTo } from "bunsane/core/ArcheType";

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

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

@HasMany("Order", { foreignKey: "user_id", nullable: true })
orders!: IOrderArcheType[];
}

@ArcheType("Order")
export class OrderArcheTypeClass extends BaseArcheType {
@ArcheTypeField(OrderInfoComponent)
info!: OrderInfoComponent;

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

Validation Patterns

GraphQL Input Schema

Important: The GraphQL schema generator can only infer simple Zod types. Complex validation chains (.min(), .max(), .email(), .regex(), .transform()) are NOT supported.

Correct approach: Use simple scalar types in the input schema, then validate manually inside the resolver.

// ✅ CORRECT: Simple scalars in input schema
@GraphQLOperation({
type: "Mutation",
input: z.object({
name: z.string(),
email: z.string(),
phone: z.string().optional(),
}),
output: UserArcheType,
})
async createUser(args: { name: string; email: string; phone?: string }, context: GraphQLContext) {
// Validate inside resolver
if (args.name.length < 2 || args.name.length > 100) {
return new GraphQLError("Name must be between 2-100 characters", {
extensions: { code: "BAD_USER_INPUT" }
});
}

const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(args.email)) {
return new GraphQLError("Invalid email format", {
extensions: { code: "BAD_USER_INPUT" }
});
}

// Proceed with validated data
const user = Entity.Create()
.add(UserTag, {})
.add(NameComponent, { value: args.name })
.add(EmailComponent, { value: args.email });
await user.save();
return user;
}

// ❌ WRONG: Complex validation in input schema (won't be inferred)
@GraphQLOperation({
type: "Mutation",
input: z.object({
name: z.string().min(2).max(100), // ❌ Not supported
email: z.string().email(), // ❌ Not supported
}),
output: UserArcheType,
})

Using ArcheType for Complex Inputs

For complex inputs based on archetypes, use getInputSchema() or withValidation():

// ✅ CORRECT: ArcheType handles schema generation properly
@GraphQLOperation({
type: "Mutation",
input: UserArcheType.getInputSchema(),
output: UserArcheType,
})
async createUser(args: IUserArcheType, context: GraphQLContext) {
const user = UserArcheType.fill(args).createEntity();
await user.save();
return user;
}

// ✅ CORRECT: ArcheType with custom field validation
@GraphQLOperation({
type: "Mutation",
input: UserArcheType.withValidation({
"info.role": z.enum([...Object.keys(UserRole)] as [string, ...string[]]),
}),
output: UserArcheType,
})

Enum Definition and Usage

Define enums using the @Enum() decorator:

import { Enum } from "bunsane/core/metadata";

@Enum()
export class OrderStatus {
static PENDING = "pending";
static PROCESSING = "processing";
static SHIPPED = "shipped";
static DELIVERED = "delivered";
static CANCELLED = "cancelled";
}

Use enums in Zod validation with Object.keys():

@GraphQLOperation({
type: "Mutation",
input: z.object({
orderId: z.string(),
status: z.enum([...Object.keys(OrderStatus)] as [string, ...string[]]),
}),
output: OrderArcheType,
})
async updateOrderStatus(args: { orderId: string; status: string }, context: GraphQLContext) {
// args.status will be one of: "PENDING", "PROCESSING", "SHIPPED", etc.
}

ArcheType with Custom Field Validation

Use ArcheType.withValidation() to add custom validators to archetype fields:

@GraphQLOperation({
type: "Mutation",
input: OrderArcheType.withValidation({
"info.status": z.enum([...Object.keys(OrderStatus)] as [string, ...string[]]),
"info.quantity": z.number().min(1, "Quantity must be at least 1"),
}),
output: OrderArcheType,
})
async createOrder(args: IOrderArcheType, context: GraphQLContext) {
const order = OrderArcheType.fill(args).createEntity();
await order.save();
return order;
}

Soft Delete Pattern

Components

@Component
export class SoftDeletedTag extends BaseComponent {}

@Component
export class DeletedAtComponent extends BaseComponent {
@CompData({ indexed: true })
value: Date = new Date();

@CompData()
deletedBy: string = "";
}

Soft Delete Operation

async function softDelete(entityId: string, deletedBy: string) {
const entity = await Entity.FindById(entityId);
if (!entity) throw new Error("Entity not found");

entity.add(SoftDeletedTag, {});
entity.add(DeletedAtComponent, {
value: new Date(),
deletedBy,
});

await entity.save();
}

Query Excluding Soft Deleted

async function listActiveUsers() {
return await new Query()
.with(UserTag)
.without(SoftDeletedTag) // Exclude soft deleted
.exec();
}

Restore Soft Deleted

async function restore(entityId: string) {
const entity = await Entity.FindById(entityId);
if (!entity) throw new Error("Entity not found");

// Remove soft delete markers
await entity.remove(SoftDeletedTag);
await entity.remove(DeletedAtComponent);

await entity.save();
}

Logging Pattern

import { logger as MainLogger } from "bunsane/core/Logger";

class OrderService extends BaseService {
private logger = MainLogger.child({ service: "OrderService" });

@GraphQLOperation({
type: "Mutation",
input: CreateOrderInput,
output: OrderArcheType,
})
async createOrder(args: any, context: GraphQLContext) {
const userId = context.jwt?.payload?.user_id;

this.logger.info({
msg: "Creating order",
userId,
itemCount: args.items?.length,
});

try {
const order = Entity.Create()
.add(OrderTag, {})
.add(OrderInfoComponent, args);

await order.save();

this.logger.info({
msg: "Order created",
orderId: order.id,
userId,
});

return order;
} catch (error) {
this.logger.error({
msg: "Failed to create order",
userId,
error: error instanceof Error ? error.message : "Unknown error",
});
throw error;
}
}
}

Batch Processing Pattern

async function processBatch<T>(
items: T[],
batchSize: number,
processor: (batch: T[]) => Promise<void>
) {
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
await processor(batch);
}
}

// Usage
const allOrders = await new Query().with(PendingOrderTag).exec();

await processBatch(allOrders, 100, async (batch) => {
await db.transaction(async (trx) => {
for (const order of batch) {
await processOrder(order, trx);
}
});
});

Rate Limiting Pattern

const rateLimits = new Map<string, { count: number; resetAt: number }>();

function checkRateLimit(key: string, limit: number, windowMs: number): boolean {
const now = Date.now();
const entry = rateLimits.get(key);

if (!entry || entry.resetAt < now) {
rateLimits.set(key, { count: 1, resetAt: now + windowMs });
return true;
}

if (entry.count >= limit) {
return false;
}

entry.count++;
return true;
}

// Usage in service
@GraphQLOperation({
type: "Mutation",
input: z.object({ email: z.string().email() }),
output: "Boolean",
})
async sendVerificationEmail(args: { email: string }, context: GraphQLContext) {
const rateLimitKey = `email:${args.email}`;

if (!checkRateLimit(rateLimitKey, 3, 60000)) { // 3 per minute
return new GraphQLError("Rate limit exceeded", {
extensions: { code: "RATE_LIMITED" }
});
}

// Send email
return true;
}