Storage Interfaces
Abstract storage classes for implementing custom database adapters
Overview
RoomKit defines 11 abstract storage classes that decouple domain logic from persistence. You must implement 9 core storages. ExamStorage and BulkOperationStorage are optional and only required when their corresponding features are enabled.
import {
LocationStorage,
RoomStorage,
BookingStorage,
RecurrenceStorage,
BlackoutStorage,
ConflictStorage,
ConfigStorage,
AuditStorage,
PriorityStorage,
ExamStorage,
BulkOperationStorage,
} from '@roomkit/nestjs';Storage Interfaces
| Interface | Manages | Required |
|---|---|---|
LocationStorage | Location nodes, hierarchy, aliases | Yes |
RoomStorage | Rooms, equipment, accessibility, partitions, operating hours | Yes |
BookingStorage | Bookings, overlapping queries, state transitions, idempotency | Yes |
RecurrenceStorage | Recurrence rules, instance management | Yes |
BlackoutStorage | Blackout windows, scope cascading | Yes |
ConflictStorage | Conflict records | Yes |
ConfigStorage | Config entries, resolution | Yes |
AuditStorage | State transition logs | Yes |
PriorityStorage | Priority tiers | Yes |
ExamStorage | Exam sessions | Optional |
BulkOperationStorage | Bulk operation tracking | Optional |
Implementation Notes
BookingStorage.createWithConflictCheck
This method must atomically create a booking and check for overlapping bookings in a single transaction. If a conflict exists, the booking must not be created.
abstract createWithConflictCheck(
booking: CreateBookingData,
options: {
checkPartitions: boolean;
checkBuffers: boolean;
},
): Promise<{ booking: Booking; conflicts: ConflictCheckResult }>;| Option | Description |
|---|---|
checkPartitions | When true, also check for conflicts with parent and child partition rooms |
checkBuffers | When true, include buffer time around bookings when checking for overlaps |
Use a database transaction to ensure atomicity. If using Prisma, wrap the create and overlap query in prisma.$transaction().
BookingStorage.updateWithVersion
Implements optimistic concurrency control. The update must only succeed if the current version in the database matches expectedVersion. If it does not match, throw StaleVersionError.
abstract updateWithVersion(
id: string,
changes: Partial<Booking>,
expectedVersion: number,
): Promise<Booking>;// Implementation pattern
const existing = await prisma.booking.findUnique({ where: { id } });
if (existing.version !== expectedVersion) {
throw new StaleVersionError({
bookingId: id,
expectedVersion,
actualVersion: existing.version,
});
}
await prisma.booking.update({
where: { id },
data: { ...changes, version: expectedVersion + 1 },
});BookingStorage.transition
Validate the state transition and create a BookingStateTransition record in a single operation. The transition record captures the previous status, new status, timestamp, and who triggered it.
abstract transition(
id: string,
from: BookingStatus,
to: BookingStatus,
triggeredBy: string,
): Promise<Booking>;BlackoutStorage.findActiveForScope
Return all active blackout windows that apply to a given location node. Must cascade up the location hierarchy -- a blackout applied to a building should be returned when querying for any room within that building.
abstract findActiveForScope(
locationNodeId: string,
timeRange: TimeRange,
): Promise<BlackoutWindow[]>;ConfigStorage.resolve
Resolve a configuration value by walking up the location hierarchy until a value is found. For example, if a room does not have a bufferMinutes config entry, check its parent floor, then building, then campus, then the global default.
abstract resolve(
key: string,
locationNodeId: string,
): Promise<ConfigEntry | null>;Custom Adapter Example
import { BookingStorage, type Booking, type CreateBookingData } from '@roomkit/nestjs';
export class MongoBookingStorage extends BookingStorage {
constructor(private readonly db: Db) {
super();
}
async createWithConflictCheck(
booking: CreateBookingData,
options: { checkPartitions: boolean; checkBuffers: boolean },
) {
const session = this.db.client.startSession();
try {
session.startTransaction();
const conflicts = await this.findOverlapping(
booking.roomId,
booking.startsAt,
booking.endsAt,
options,
session,
);
const created = await this.db.collection('bookings').insertOne(
{ ...booking, version: 1, status: 'requested' },
{ session },
);
await session.commitTransaction();
return { booking: created, conflicts };
} finally {
await session.endSession();
}
}
// ... implement remaining abstract methods
}