My App
RoomKitBackend

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

InterfaceManagesRequired
LocationStorageLocation nodes, hierarchy, aliasesYes
RoomStorageRooms, equipment, accessibility, partitions, operating hoursYes
BookingStorageBookings, overlapping queries, state transitions, idempotencyYes
RecurrenceStorageRecurrence rules, instance managementYes
BlackoutStorageBlackout windows, scope cascadingYes
ConflictStorageConflict recordsYes
ConfigStorageConfig entries, resolutionYes
AuditStorageState transition logsYes
PriorityStoragePriority tiersYes
ExamStorageExam sessionsOptional
BulkOperationStorageBulk operation trackingOptional

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 }>;
OptionDescription
checkPartitionsWhen true, also check for conflicts with parent and child partition rooms
checkBuffersWhen 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
}

On this page