@fufulog/composable-gym-schedule
v0.1.0
Published
Modular, headless scheduling, recurring template engine, date overrides, and booking management for gyms, studios, and fitness facilities.
Maintainers
Readme
@fufulog/composable-gym-schedule
Modular, headless scheduling and booking management engine designed for gyms, studios, and fitness facilities.
Pure TypeScript, zero frontend dependencies, and completely agnostic to database or framework.
Features
- Open Data Models: No restrictive generics or rigid schemas. Extend base properties with custom business attributes (
tags,price,skillLevel,color, etc.). - Smart Merge Algorithm: Combines recurring weekly templates with one-off date overrides (
cancelled,rescheduled,added), handles date-boundary-crossing reschedules, and computes real-timebookedCountandspotsLeft. - Local Date Math: Avoids UTC midnight day-shift drift issues by computing calendar dates strictly against local components.
- Pluggable Storage Adapters:
SqlAdapter: Works with parameterized SQL runners (node-postgres,postgres.js,bun:sqlite,mysql2) with customizable table and column mappings.DrizzleAdapter: Works with Drizzle ORM PostgreSQL databases (@fufulog/composable-gym-schedule/drizzle).
- Web Standard HTTP Controller: Built on standard
Request->Responsefetch API, compatible with Elysia, Hono, Express, Next.js route handlers, and Bun.serve. - Unhosted / Freeform Support: Strict adherence to YAGNI:
instructorId: nullrepresents unhosted sessions (e.g. open gym, freeform practice).
Installation
npm install @fufulog/composable-gym-schedule
# or
bun add @fufulog/composable-gym-scheduleQuick Start (Core Merge Engine)
import { mergeScheduleSlots } from "@fufulog/composable-gym-schedule";
const slots = mergeScheduleSlots({
templates: [
{
id: 1,
dayOfWeek: "Monday",
startTime: "09:00am",
endTime: "10:00am",
capacity: 10,
sessionId: 101,
instructorId: 5,
instructorName: "Sarah Connor"
}
],
sessions: [
{
id: 101,
name: "Power Reformer",
price: "$35",
level: "Intermediate"
}
],
overrides: [],
startDateStr: "2026-09-07",
endDateStr: "2026-09-13"
});
console.log(slots);Storage Adapters & ScheduleEngine
import { ScheduleEngine, SqlAdapter } from "@fufulog/composable-gym-schedule";
import { pool } from "./db";
const adapter = new SqlAdapter({
query: async (sql, params) => {
const res = await pool.query(sql, params);
return res.rows;
},
tables: {
sessions: "classes",
templates: "class_templates",
overrides: "class_overrides",
bookings: "class_bookings"
},
columns: {
sessionId: "class_id",
cancellationMode: "boolean"
}
});
const engine = new ScheduleEngine({
adapter,
cancellationCutoffHours: 12,
reminderWindowHours: 24
});
// Fetch merged schedule for next week
const schedule = await engine.getMergedSchedule("2026-09-07", "2026-09-14");
// Reserve a slot with atomic capacity enforcement
const booking = await engine.reserveSlot({
sessionId: 101,
templateId: 1,
dateStr: "2026-09-07",
timeStr: "09:00am - 10:00am",
userId: 42,
userEmail: "[email protected]",
userName: "Jane Doe"
});HTTP Controller
import { ScheduleController } from "composable-gym-schedule";
const controller = new ScheduleController({
engine,
authBridge: {
getUser: async (req) => req.user,
isAdmin: (user) => user?.role === "admin"
}
});
// In Elysia / Hono / Next.js / Bun:
// export default { fetch: (req) => controller.handle(req) };License
MIT
