CourseKitGuides
Custom Storage Adapter
Build a custom storage adapter for any database
Overview
CourseKit uses abstract storage classes as its storage interface. You can implement these classes to use any database — SQL, NoSQL, in-memory, or a remote API.
Step 1: Choose Your Storage Classes
CourseKit has 8 storage interfaces. You must implement at least 7 (all except LocationDistanceStorage, which is optional):
| Interface | Manages |
|---|---|
TimetableEventStorage | Events, exceptions, instructor/group associations |
RoomStorage | Rooms |
InstructorStorage | Instructors |
GroupStorage | Groups and student memberships |
AvailabilityStorage | Availability rules |
AcademicPeriodStorage | Academic periods |
CourseStorage | Courses |
LocationDistanceStorage | Campus travel distances (optional) |
Step 2: Implement TimetableEventStorage
This is the most complex adapter with 12 abstract methods:
import { TimetableEventStorage } from '@hfu.digital/coursekit-nestjs';
import type {
TimetableEvent, EventException, EventInstructor, EventGroup,
ScheduleQuery,
} from '@hfu.digital/coursekit-nestjs';
export class DrizzleTimetableEventAdapter extends TimetableEventStorage {
constructor(private readonly db: DrizzleDB) {
super();
}
async create(data: Omit<TimetableEvent, 'id' | 'createdAt' | 'updatedAt' | 'version'>) {
const [event] = await this.db.insert(events).values({
...data,
id: crypto.randomUUID(),
version: 0,
}).returning();
return event;
}
async findById(id: string) {
return this.db.query.events.findFirst({ where: eq(events.id, id) }) ?? null;
}
async findByQuery(query: ScheduleQuery) {
// Build WHERE clauses from dateRange + filters
// Return matching events
}
async update(id: string, data: Partial<TimetableEvent>, expectedVersion?: number) {
if (expectedVersion !== undefined) {
// Check version for optimistic concurrency
}
const [updated] = await this.db.update(events)
.set({ ...data, updatedAt: new Date() })
.where(eq(events.id, id))
.returning();
return updated;
}
async delete(id: string) {
await this.db.delete(events).where(eq(events.id, id));
}
// Exception management
async createException(data: Omit<EventException, 'id'>) { /* ... */ }
async findExceptions(eventId: string) { /* ... */ }
async deleteException(id: string) { /* ... */ }
// Instructor associations
async addInstructor(data: Omit<EventInstructor, 'id'>) { /* ... */ }
async removeInstructor(eventId: string, instructorId: string) { /* ... */ }
async findInstructors(eventId: string) { /* ... */ }
// Group associations
async addGroup(data: Omit<EventGroup, 'id'>) { /* ... */ }
async removeGroup(eventId: string, groupId: string) { /* ... */ }
async findGroups(eventId: string) { /* ... */ }
}Step 3: Implement Simpler Adapters
The remaining adapters are straightforward CRUD. For example:
import { RoomStorage } from '@hfu.digital/coursekit-nestjs';
import type { Room } from '@hfu.digital/coursekit-nestjs';
export class DrizzleRoomAdapter extends RoomStorage {
constructor(private readonly db: DrizzleDB) {
super();
}
async create(data: Omit<Room, 'id'>) {
const [room] = await this.db.insert(rooms)
.values({ ...data, id: crypto.randomUUID() })
.returning();
return room;
}
async findById(id: string) {
return this.db.query.rooms.findFirst({ where: eq(rooms.id, id) }) ?? null;
}
async findAll(filters?) {
let query = this.db.select().from(rooms);
if (filters?.building) query = query.where(eq(rooms.building, filters.building));
if (filters?.minCapacity) query = query.where(gte(rooms.capacity, filters.minCapacity));
return query;
}
async update(id: string, data: Partial<Room>) {
const [updated] = await this.db.update(rooms)
.set(data)
.where(eq(rooms.id, id))
.returning();
return updated;
}
async delete(id: string) {
await this.db.delete(rooms).where(eq(rooms.id, id));
}
}Step 4: Register with CourseKit
import { CourseKitModule } from '@hfu.digital/coursekit-nestjs';
@Module({
imports: [
CourseKitModule.register({
eventStorage: new DrizzleTimetableEventAdapter(db),
roomStorage: new DrizzleRoomAdapter(db),
instructorStorage: new DrizzleInstructorAdapter(db),
groupStorage: new DrizzleGroupAdapter(db),
availabilityStorage: new DrizzleAvailabilityAdapter(db),
periodStorage: new DrizzlePeriodAdapter(db),
courseStorage: new DrizzleCourseAdapter(db),
}),
],
})
export class AppModule {}Tips
- Use
crypto.randomUUID()for ID generation findByQuery()must filter bydateRangeat minimum — the other filters are optionalAvailabilityStorage.findByEntityInRange()should return rules whose time windows overlap the given date rangeGroupStorage.findStudents()returnsStudentGroup[]records, notStudent[]objects- The
expectedVersionparameter onTimetableEventStorage.update()is optional — skip it if your database doesn't support optimistic concurrency
Testing Custom Adapters
Use the in-memory adapters from @hfu.digital/coursekit-nestjs/testing as a reference implementation. They show the expected behavior for every method.