@molecule/api-resource-event-type
API resource · resource-event-type · API (Node) · v1.0.3 · Apache-2.0
Calendly-style scheduling event types + availability
npm install @molecule/api-resource-event-typeHow it works
@molecule/api-resource-event-type is an API resource: the routes, validation and storage for resource-event-type, built on the database and auth cores so it runs on whichever providers your app has bonded.
import { createEventTypeRouter } from '@molecule/api-resource-event-type'
app.use('/event-types', createEventTypeRouter())Works with: @molecule/api-bonds-default-express, @molecule/api-database, @molecule/api-i18n, @molecule/api-middleware-validation
Reference
@molecule/api-resource-event-type — Calendly-style event-types +
weekly availability rules + slot generation.
Extracted from the meeting-scheduler flagship.
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Quick Start
import { createEventTypeRouter } from '@molecule/api-resource-event-type'
app.use('/event-types', createEventTypeRouter())
import {
createEventTypeForOwner,
generateSlots,
listAvailabilityRulesForUser,
setAvailabilityRulesForUser,
} from '@molecule/api-resource-event-type'
await setAvailabilityRulesForUser(userId, [
{ day_of_week: 1, start_minute: 540, end_minute: 1020, timezone: 'America/New_York' },
])
await createEventTypeForOwner(userId, {
name: '30-min consult',
slug: '30-min',
duration_minutes: 30,
})
const rules = await listAvailabilityRulesForUser(userId)
const slots = generateSlots({ date: '2026-06-15', durationMinutes: 30, rules })
Type
resource
Installation
npm install @molecule/api-resource-event-type @molecule/api-bonds-default-express @molecule/api-database @molecule/api-i18n @molecule/api-middleware-validation express zod
npm install -D @types/express
API
Interfaces
EventTypeRow
Database row shape for a bookable event type owned by a user.
interface EventTypeRow {
id: string
owner_id: string
name: string
slug: string
description: string | null
duration_minutes: number
location_kind: LocationKind
location_value: unknown
buffer_before_minutes: number
buffer_after_minutes: number
min_notice_minutes: number
max_per_day: number | null
requires_confirmation: boolean
color: string | null
is_active: boolean
position: number
created_at: string | Date
updated_at: string | Date
}
AvailabilityRuleRow
Database row shape for a recurring weekly availability rule belonging to a user.
interface AvailabilityRuleRow {
id: string
user_id: string
day_of_week: number // 0=Sunday, 6=Saturday
start_minute: number // minutes since midnight, local time
end_minute: number
timezone: string
created_at: string | Date
}
AvailabilitySlot
A discrete time window indicating whether a booking slot is open or blocked.
interface AvailabilitySlot {
start: string // ISO datetime
end: string
available: boolean
}
Types
LocationKind
The medium through which an event type meeting takes place.
type LocationKind = 'video' | 'phone' | 'in_person' | 'custom'
Functions
createEventTypeRouter()
Creates and returns the Express router for event-type and availability endpoints.
function createEventTypeRouter(): Router
listEventTypesForOwner(ownerId, opts?)
List all event types owned by the given user, optionally including inactive ones.
function listEventTypesForOwner(
ownerId: string,
opts?: { include_inactive?: boolean },
): Promise<EventTypeRow[]>
getEventTypeBySlug(slug)
Look up an active event type by its URL slug.
function getEventTypeBySlug(slug: string): Promise<EventTypeRow | null>
getEventTypeForOwner(eventTypeId, ownerId)
Fetch a single event type by ID, returning null if it does not belong to the given owner.
function getEventTypeForOwner(eventTypeId: string, ownerId: string): Promise<EventTypeRow | null>
createEventTypeForOwner(ownerId, data)
Create a new event type for the given owner with the supplied configuration.
function createEventTypeForOwner(
ownerId: string,
data: {
name: string
slug: string
description?: string | null
duration_minutes?: number
location_kind?: LocationKind
location_value?: unknown
buffer_before_minutes?: number
buffer_after_minutes?: number
min_notice_minutes?: number
max_per_day?: number | null
requires_confirmation?: boolean
color?: string | null
is_active?: boolean
position?: number
},
): Promise<EventTypeRow>
updateEventTypeForOwner(eventTypeId, ownerId, patch)
Apply a partial patch to an event type, returning null if the record does not belong to the owner.
function updateEventTypeForOwner(
eventTypeId: string,
ownerId: string,
patch: Partial<EventTypeRow>,
): Promise<EventTypeRow | null>
deleteEventTypeForOwner(eventTypeId, ownerId)
Delete an event type by ID, returning false if it does not belong to the given owner.
function deleteEventTypeForOwner(eventTypeId: string, ownerId: string): Promise<boolean>
listAvailabilityRulesForUser(userId)
Return all availability rules for the given user, ordered by day and start time.
function listAvailabilityRulesForUser(userId: string): Promise<AvailabilityRuleRow[]>
setAvailabilityRulesForUser(userId, rules)
Replace all availability rules for the given user with the supplied set.
function setAvailabilityRulesForUser(
userId: string,
rules: { day_of_week: number; start_minute: number; end_minute: number; timezone: string }[],
): Promise<AvailabilityRuleRow[]>
generateSlots(opts)
Generate available slots for a given date based on the user's availability rules + event-type duration + buffers.
Caller is responsible for filtering against existing bookings.
function generateSlots(opts: {
date: string
durationMinutes: number
bufferBeforeMinutes?: number
bufferAfterMinutes?: number
rules: Pick<AvailabilityRuleRow, 'day_of_week' | 'start_minute' | 'end_minute'>[]
}): AvailabilitySlot[]
Constants
LOCATION_KINDS
Allowed location kinds for an event type.
const LOCATION_KINDS: readonly ['video', 'phone', 'in_person', 'custom']
slugRegex
Regex that validates a lowercase kebab-case slug (no leading/trailing hyphens).
const slugRegex: RegExp
eventTypeCreateSchema
Zod schema for validating the request body when creating a new event type.
const eventTypeCreateSchema: z.ZodObject<
{
name: z.ZodString
slug: z.ZodString
description: z.ZodOptional<z.ZodNullable<z.ZodString>>
duration_minutes: z.ZodOptional<z.ZodNumber>
location_kind: z.ZodOptional<
z.ZodEnum<{ video: 'video'; phone: 'phone'; in_person: 'in_person'; custom: 'custom' }>
>
location_value: z.ZodOptional<z.ZodUnknown>
buffer_before_minutes: z.ZodOptional<z.ZodNumber>
buffer_after_minutes: z.ZodOptional<z.ZodNumber>
min_notice_minutes: z.ZodOptional<z.ZodNumber>
max_per_day: z.ZodOptional<z.ZodNullable<z.ZodNumber>>
requires_confirmation: z.ZodOptional<z.ZodBoolean>
color: z.ZodOptional<z.ZodNullable<z.ZodString>>
is_active: z.ZodOptional<z.ZodBoolean>
position: z.ZodOptional<z.ZodNumber>
},
z.core.$strip
>
eventTypeUpdateSchema
Zod schema for validating the request body when updating an existing event type (all fields optional).
const eventTypeUpdateSchema: z.ZodObject<
{
name: z.ZodOptional<z.ZodString>
slug: z.ZodOptional<z.ZodString>
description: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodString>>>
duration_minutes: z.ZodOptional<z.ZodOptional<z.ZodNumber>>
location_kind: z.ZodOptional<
z.ZodOptional<
z.ZodEnum<{ video: 'video'; phone: 'phone'; in_person: 'in_person'; custom: 'custom' }>
>
>
location_value: z.ZodOptional<z.ZodOptional<z.ZodUnknown>>
buffer_before_minutes: z.ZodOptional<z.ZodOptional<z.ZodNumber>>
buffer_after_minutes: z.ZodOptional<z.ZodOptional<z.ZodNumber>>
min_notice_minutes: z.ZodOptional<z.ZodOptional<z.ZodNumber>>
max_per_day: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodNumber>>>
requires_confirmation: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>
color: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodString>>>
is_active: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>
position: z.ZodOptional<z.ZodOptional<z.ZodNumber>>
},
z.core.$strip
>
availabilityQuerySchema
Zod schema for the query parameters when fetching availability slots (requires a YYYY-MM-DD date).
const availabilityQuerySchema: z.ZodObject<{ date: z.ZodString }, z.core.$strip>
availabilityRuleSchema
Zod schema for a single availability rule defining a recurring weekly time block.
const availabilityRuleSchema: z.ZodObject<
{
day_of_week: z.ZodNumber
start_minute: z.ZodNumber
end_minute: z.ZodNumber
timezone: z.ZodString
},
z.core.$strip
>
Injection Notes
Requirements
Peer dependencies:
@molecule/api-bonds-default-express^1.0.1@molecule/api-database^1.0.1@molecule/api-i18n^1.0.1@molecule/api-middleware-validation^1.0.1express^5.0.0zod^4.0.0
Runtime Dependencies
-
@molecule/api-bonds-default-express -
@molecule/api-database -
@molecule/api-i18n -
@molecule/api-middleware-validation -
express -
zod -
Migration required.
src/__setup__/event_types.sqlships with this package (tablesevent_types,availability_rules) and must exist in the target database before use (scaffolded apps apply it automatically). -
Mounting differs from sibling resources: this package exports an Express
RouterFACTORY —app.use('/event-types', createEventTypeRouter())— not a declarativeroutesarray +requestHandlerMap. It requires the Express default bonds (requireUser/getParamIdfrom@molecule/api-bonds-default-express) to be wired. -
The by-slug routes are PUBLIC by design (
GET /by-slug/:slugand…/availability?date=YYYY-MM-DDpower the public booking page — no auth, no owner data beyond the published event type). Everything else is owner-scoped via the session; never accept an owner id from the client. -
generateSlots()is timezone-naive and booking-blind. It matches rules by the UTC day-of-week ofdateand returns minute-of-day slots straight from the rules — the storedtimezoneis NOT applied, and existing bookings are NOT subtracted. Convert for display in the app, and filter taken slots against your booking/meeting store before offering them.
E2E Tests
Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:
- Creating an event type in the UI persists its real fields (name, slug, duration_minutes, location_kind, buffer_before/after) — reopen it and confirm each round-trips — and it appears in the owner's catalog list (GET /) with a shareable booking link at its slug (GET /by-slug/:slug resolves to the same event type).
- Availability + duration produce the right slot grid: set weekly rules to a 9am-5pm window (start_minute 540, end_minute 1020) and open the public availability for a matching weekday (GET /by-slug/:slug/availability?date= YYYY-MM-DD). A 30-min type yields back-to-back :00/:30 slots filling the window (16 of them) and no slot spills past end_minute.
- Buffers are respected: adding buffer_before/after widens the per-slot stride (duration + before + after), so the grid thins accordingly — a 30-min type with a 15-min after-buffer no longer packs two slots per hour.
- Active/published gating works: an INACTIVE event type (is_active=false) 404s from the public /by-slug/:slug AND its /availability; re-activating makes the booking link resolve again. (is_active IS the publish switch — there is no separate published flag.)
- Editing changes FUTURE slots: change duration_minutes (PUT /:id) or the availability window (PUT /availability/rules) and re-query availability — the returned grid reflects the new duration/window immediately.
- Timezone renders correctly, not shifted: rules carry a timezone but generateSlots returns minute-of-day slots in UTC without applying it — confirm the booking view converts each slot into the intended timezone (a 9:00 local rule shows a 9:00 slot to the invitee, not a UTC-offset time), and changing the tz re-renders rather than silently re-timing the rule.
- Authorization — a signed-in owner manages ONLY their own event types: GET/PUT/DELETE /:id with another owner's id returns 404 (never the row, never an edit), and the client never supplies owner_id (it comes from the session). The public /by-slug view exposes only active types and returns only the event type + its generated slots — never the owner's raw availability rules or any inactive/private config beyond what booking needs.